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

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.wvp.config.FfmpegConfig;
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.docking.DockingService;
import cn.skcks.docking.gb28181.wvp.service.ffmpeg.FfmpegSupportService;
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.*;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.FrameGrabber;
import org.springframework.stereotype.Service;

import java.io.*;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

@Slf4j
@Service
@RequiredArgsConstructor
public class VideoService {
    private final FfmpegSupportService ffmpegSupportService;
    private final WvpProxyConfig wvpProxyConfig;
    private final ProxySipConfig proxySipConfig;
    private final DockingService dockingService;
    private final SipSender sender;
    private final FfmpegConfig ffmpegConfig;
    private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

    /**
     * 写入 flv 响应头信息
     * @param response  HttpServletResponse 响应
     */
    public void header(HttpServletResponse response) {
        response.setContentType("video/mp4");
        response.setHeader("Accept-Ranges", "none");
        response.setHeader("Connection", "close");
        response.setHeader("Content-Disposition","attachment; filename=\"record.mp4\"");
    }

    /**
     * 录制视频 并写入 响应
     * @param request HttpServletRequest 请求
     * @param response HttpServletResponse 同步响应
     * @param url 要录制的视频地址
     * @param time 录制时长 (单位: 秒)
     */
    @SneakyThrows
    public void record(HttpServletRequest request, HttpServletResponse response, String url, long time) {
        AsyncContext asyncContext = request.startAsync();
        asyncContext.setTimeout(0);
        asyncContext.start(() -> {
            header(response);
            try{
                record(asyncContext.getResponse(), url, time);
            } finally {
                log.info("record 结束");
                asyncContext.complete();
            }
        });
    }

    /**
     * 录制视频 并写入 异步响应
     * @param response AsyncContext.getResponse 异步响应
     * @param url 要录制的视频地址
     * @param time 录制时长 (单位: 秒)
     */
    @SneakyThrows
    public void record(ServletResponse response, String url, long time) {
        if (wvpProxyConfig.getUseFfmpeg()) {
            ffmpegRecord(response, url, time);
        } else {
            javaCVrecord(response, url, time);
        }
    }

    @RequiredArgsConstructor
    public class FfmpegExecuteResultHandler extends DefaultExecuteResultHandler implements ExecuteResultHandler {
        private final static long SLEEP_TIME_MS = 50;
        @Setter(AccessLevel.PRIVATE)
        private boolean hasResult = false;
        private final WvpProxyDevice device;
        private final String callId;
        private final SipSender sender;

        private void mediaStatus(){
            String deviceId = device.getGbDeviceId();
            Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(deviceId);
            if(deviceByGbDeviceId.isEmpty()){
                return;
            }
            WvpProxyDocking wvpProxyDocking = deviceByGbDeviceId.get();
            String ip = wvpProxyDocking.getIp();
            int port = Integer.parseInt(wvpProxyDocking.getPort());
            try{
                sender.sendRequest((provider,localIp,localPort)->
                        SipRequestBuilder.createByeRequest(ip, port, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, callId));
            }catch (Exception e){
                log.error("bye 请求发送失败 {}",e.getMessage());
            }
        }

        public boolean hasResult() {
            return hasResult;
        }

        @SneakyThrows
        public void waitFor() {
            while (!hasResult()) {
                Thread.sleep(SLEEP_TIME_MS);
            }
        }

        @Override
        public void onProcessComplete(int exitValue) {
            hasResult = true;
            mediaStatus();
        }

        @Override
        public void onProcessFailed(ExecuteException e) {
            hasResult = true;
            mediaStatus();
        }
    }

    public FfmpegExecuteResultHandler mediaStatus(WvpProxyDevice device, String key){
        return new FfmpegExecuteResultHandler(device,key,sender);
    }

    /**
     * 录制视频 并写入 异步响应
     * @param response AsyncContext.getResponse 异步响应
     * @param url 要录制的视频地址
     * @param time 录制时长 (单位: 秒)
     */
    @SneakyThrows
    public void javaCVrecord(ServletResponse response, String url, long time) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        OutputStream outputStream = response.getOutputStream();
        log.info("准备录制 url {}, time: {}", url, time);
        // FFmpeg 调试日志
//        FFmpegLogCallback.set();
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(url);
        grabber.start();

        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(stream, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
        recorder.setInterleaved(true);
        recorder.setVideoOption("preset", "ultrafast");
        recorder.setVideoOption("tune", "zerolatency");
        recorder.setVideoOption("crf", "25");
        recorder.setFrameRate(grabber.getFrameRate());
        recorder.setSampleRate(grabber.getSampleRate());
        recorder.setOption("flvflags", "no_duration_filesize");
        recorder.setOption("movflags","frag_keyframe+empty_moov");
        if (grabber.getAudioChannels() > 0) {
            recorder.setAudioChannels(grabber.getAudioChannels());
            recorder.setAudioBitrate(grabber.getAudioBitrate());
            recorder.setAudioCodec(grabber.getAudioCodec());
        }
        recorder.setVideoBitrate(grabber.getVideoBitrate());
//        recorder.setVideoCodec(grabber.getVideoCodec());
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); // 视频源数据yuv
        recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); // 设置音频压缩方式
        recorder.setFormat("mp4");
        recorder.setVideoOption("threads", String.valueOf(Runtime.getRuntime().availableProcessors())); // 解码线程数
        recorder.start(grabber.getFormatContext());
        log.info("开始录制 {}", url);

        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        AtomicBoolean record = new AtomicBoolean(true);
        scheduledExecutorService.schedule(() -> {
            log.info("到达结束时间, 结束录制 {}", url);
            record.set(false);
        }, time, TimeUnit.SECONDS);
        try {
            AVPacket k;
            while (record.get() && (k = grabber.grabPacket()) != null) {
                recorder.recordPacket(k);
                if(stream.size() > 0){
                    outputStream.write(stream.toByteArray());
                    outputStream.flush();
                    stream.reset();
                }
                avcodec.av_packet_unref(k);
            }
            grabber.close();
            recorder.close();
        } catch (FFmpegFrameRecorder.Exception | FrameGrabber.Exception e) {
            throw new RuntimeException(e);
        } catch (IOException ignore){}
        finally {
            log.info("结束录制 {}", url);
            stream.close();
            outputStream.close();
        }
    }

    /**
     * 录制视频 并写入 异步响应
     * @param response AsyncContext.getResponse 异步响应
     * @param url 要录制的视频地址
     * @param time 录制时长 (单位: 秒)
     */
    @SneakyThrows
    public void ffmpegRecord(ServletResponse response, String url, long time, WvpProxyDevice device,String callId){
        String tmpDir = ffmpegConfig.getTmpDir();
        String fileName = callId + ".mp4";
        File file = new File(tmpDir, fileName);
        Executor executor;
        DefaultExecuteResultHandler executeResultHandler = mediaStatus(device,callId);
        if(ffmpegConfig.getUseTmpFile()) {
            OutputStream outputStream = new PipedOutputStream();
            String filePath = file.getAbsolutePath();
            PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, System.err);
            if(proxySipConfig.isUsePlaybackToDownload()){
                executor = ffmpegSupportService.playbackToStream(url, filePath, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
            } else {
                executor = ffmpegSupportService.downloadToStream(url, filePath, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
            }
        } else {
            OutputStream outputStream = response.getOutputStream();
            PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, System.err);
            if(proxySipConfig.isUsePlaybackToDownload()){
                executor = ffmpegSupportService.playbackToStream(url, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
            } else {
                executor = ffmpegSupportService.downloadToStream(url, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
            }
        }

        DateTime startTime = DateUtil.date();
        log.info("开始录制 {}", url);
        ScheduledFuture<?> schedule = scheduledExecutorService.schedule(() -> {
            log.info("到达结束时间, 结束录制 {}", url);
            executor.getWatchdog().destroyProcess();
            log.info("结束录制 {}", url);
        }, time + 60, TimeUnit.SECONDS);
        executeResultHandler.waitFor();
        schedule.cancel(true);
        DateTime endTime = DateUtil.date();
        log.info("录制进程结束 {}, 录制耗时: {}", url, DateUtil.between(startTime,endTime, DateUnit.SECOND));

        if(ffmpegConfig.getUseTmpFile()) {
            ServletOutputStream servletOutputStream = response.getOutputStream();
            try{
                log.info("临时文件 {}(大小 {})", file.getAbsolutePath(), file.length());
                IoUtil.copy(new FileInputStream(file), servletOutputStream);
                response.flushBuffer();
            } catch (Exception e){
                log.error("写入 http 响应异常: {}", e.getMessage());
            } finally {
                System.gc();
                boolean delete = file.delete();
                log.info("删除临时文件 {} => {}", file, delete);
            }
        }
    }

    /**
     * 录制视频 并写入 异步响应
     * @param response AsyncContext.getResponse 异步响应
     * @param url 要录制的视频地址
     * @param time 录制时长 (单位: 秒)
     */
    @SneakyThrows
    public void ffmpegRecord(ServletResponse response, String url, long time){
        ServletOutputStream outputStream = response.getOutputStream();
        ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
        PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
        DefaultExecuteResultHandler defaultExecuteResultHandler = new DefaultExecuteResultHandler();
        DateTime startTime = DateUtil.date();
        Executor executor;
        if(proxySipConfig.isUsePlaybackToDownload()){
            executor = ffmpegSupportService.playbackToStream(url, time, TimeUnit.SECONDS,streamHandler,defaultExecuteResultHandler);
        } else {
            executor = ffmpegSupportService.downloadToStream(url, time, TimeUnit.SECONDS,streamHandler,defaultExecuteResultHandler);
        }
        log.info("开始录制 {}", url);
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        ScheduledFuture<?> schedule = scheduledExecutorService.schedule(() -> {
            log.info("到达结束时间, 结束录制 {}", url);
            executor.getWatchdog().destroyProcess();
            log.info("结束录制 {}", url);
        }, time, TimeUnit.SECONDS);
        defaultExecuteResultHandler.waitFor();
        schedule.cancel(true);
        DateTime endTime = DateUtil.date();
        log.info("录制进程结束 {}, 录制耗时: {}", url, DateUtil.between(startTime,endTime, DateUnit.SECOND));
    }
}
