package com.ycwl.basic.task;

import cn.hutool.core.thread.ThreadFactoryBuilder;
import com.ycwl.basic.biz.FaceStatusManager;
import com.ycwl.basic.biz.OrderBiz;
import com.ycwl.basic.constant.StorageConstant;
import com.ycwl.basic.device.DeviceFactory;
import com.ycwl.basic.device.entity.common.FileObject;
import com.ycwl.basic.device.operator.IDeviceStorageOperator;
import com.ycwl.basic.enums.FaceCutStatus;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.MemberRelationRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.utils.SnowFlakeUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@Component
@EnableScheduling
@Slf4j
public class VideoPieceGetter {
    @Autowired
    private FaceSampleMapper faceSampleMapper;
    @Autowired
    private FaceRepository faceRepository;
    @Autowired
    private DeviceRepository deviceRepository;
    @Autowired
    private SourceMapper sourceMapper;
    @Autowired
    private OrderBiz orderBiz;
    @Autowired
    private TemplateRepository templateRepository;
    @Autowired
    private ScenicRepository scenicRepository;
    @Autowired
    private DeviceIntegrationService deviceIntegrationService;
    @Autowired
    private MemberRelationRepository memberRelationRepository;

    public static final String PROBE_SIZE = "16M";
    @Autowired
    private FaceStatusManager faceStatusManager;

    /**
     * 景区设备配对关系缓存
     * key: scenicId
     * value: Map<deviceId, pairDeviceId>，空Map表示该景区无配对关系
     */
    private final Cache<Long, Map<Long, Long>> pairDeviceCache = Caffeine.newBuilder()
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .maximumSize(500)
            .build();

    @Data
    public static class Task {
        public List<Long> faceSampleIds = new ArrayList<>();
        public Callback callback;
        public Long memberId;
        public Long faceId;
        public Long templateId;
        public boolean force;

        public interface Callback {
            void onInvoke();
        }
    }
    @Data
    public static class FfmpegTask {
        List<FileObject> fileList;
        BigDecimal duration;
        BigDecimal offsetStart;
        String outputFile;
    }

    public static ConcurrentLinkedQueue<Task> queue = new ConcurrentLinkedQueue<>();

    public static void addTask(Task task) {
        queue.add(task);
    }

    @Scheduled(fixedRate = 200L)
    public void doTask() {
        Task task = queue.poll();
        if (task == null) {
            return;
        }
        log.info("poll task: {}/{}", task, queue.size());
        Thread.ofVirtual().start(() -> {
            try {
                runTask(task);
            } catch (Exception e) {
                log.error("run task error", e);
            }
        });
    }

    private void runTask(Task task) {
        List<String> templatePlaceholder;
        if (null != task.getTemplateId()) {
            templatePlaceholder = templateRepository.getTemplatePlaceholder(task.getTemplateId());
        } else {
            templatePlaceholder = null;
        }
        // 临时处理，只有逻辑层自己设置是否正在切片后再做更新
//        if (task.faceId != null) {
//            taskStatusBiz.setFaceCutStatus(task.faceId, 0);
//        }
        AtomicBoolean invoke = new AtomicBoolean(false);
        // 为了批量定时停止
        final ThreadFactory threadFactory = new ThreadFactoryBuilder()
                .setNamePrefix("VPG-" + task.faceId + "-t")
                .build();
        final ThreadPoolExecutor executor = new ThreadPoolExecutor(16, 128, 0L, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(128),
                threadFactory
        );
        Map<String, AtomicInteger> currentUnFinPlaceholder = new ConcurrentHashMap<>();
        List<FaceSampleEntity> list = faceSampleMapper.listByIds(task.getFaceSampleIds());
        if (list == null || list.isEmpty()) {
            task.callback.onInvoke();
            return;
        }
        Long scenicId = list.getFirst().getScenicId();
        Map<Long, Long> pairDeviceMap = pairDeviceCache.get(scenicId, this::loadPairDeviceMap);
        Map<Long, List<FaceSampleEntity>> collection = list.stream()
                .filter(faceSample -> {
                    if (templatePlaceholder != null) {
                        return templatePlaceholder.contains(faceSample.getDeviceId().toString());
                    }
                    return true;
                })
                .collect(Collectors.groupingBy(FaceSampleEntity::getDeviceId));
        if (templatePlaceholder != null) {
            templatePlaceholder.forEach(deviceId -> {
                currentUnFinPlaceholder.computeIfAbsent(deviceId, k -> new AtomicInteger(0)).incrementAndGet();
            });
            if (log.isDebugEnabled()) {
                log.debug("[Placeholder初始化] 有templateId，初始化完成：placeholder总数={}, 不同设备数={}, 详细计数={}",
                        templatePlaceholder.size(),
                        currentUnFinPlaceholder.size(),
                        currentUnFinPlaceholder.entrySet().stream()
                                .map(e -> e.getKey() + "=" + e.getValue().get())
                                .collect(Collectors.joining(", ")));
            }
        } else {
            collection.keySet().forEach(deviceId -> {
                currentUnFinPlaceholder.put(deviceId.toString(), new AtomicInteger(1));
            });
            log.debug("[Placeholder初始化] 无templateId，初始化完成：设备数={}",
                    currentUnFinPlaceholder.size());
        }
        collection.values().forEach(faceSampleList -> {
            faceSampleList.forEach(faceSample -> {
                executor.execute(() -> {
                    // 处理关联设备：如果当前设备是某个主设备的配对设备，也处理主设备
                    if (pairDeviceMap.containsValue(faceSample.getDeviceId())) {
                        pairDeviceMap.entrySet().stream()
                                .filter(entry -> entry.getValue().equals(faceSample.getDeviceId()))
                                .map(Map.Entry::getKey).forEach(pairDeviceId -> {
                                    log.info("找到同景区关联设备：{} -> {}", pairDeviceId, faceSample.getDeviceId());
                                    if (pairDeviceId != null) {
                                        doCut(pairDeviceId, faceSample.getId(), faceSample.getCreateAt(), task);
                                        // 让主设备的计数器 -1
                                        AtomicInteger pairCount = currentUnFinPlaceholder.get(pairDeviceId.toString());
                                        if (pairCount != null) {
                                            int remaining = pairCount.decrementAndGet();
//                                            log.info("[计数器更新] 关联设备 {} 计数器递减，剩余={}, currentUnFinPlaceholder总数={}",
//                                                    pairDeviceId, remaining, currentUnFinPlaceholder.size());
                                            if (remaining <= 0) {
                                                currentUnFinPlaceholder.remove(pairDeviceId.toString());
//                                                log.debug("[Placeholder完成] 设备 {} 的placeholder已满足并移除，剩余设备数={}",
//                                                        pairDeviceId, currentUnFinPlaceholder.size());
                                            }
                                        }
                                    }
                                });
                    }

                    // 处理当前设备
                    doCut(faceSample.getDeviceId(), faceSample.getId(), faceSample.getCreateAt(), task);
                    AtomicInteger count = currentUnFinPlaceholder.get(faceSample.getDeviceId().toString());
                    if (count != null) {
                        int remaining = count.decrementAndGet();
//                        log.info("[计数器更新] 设备 {} 计数器递减，剩余={}, currentUnFinPlaceholder总数={}",
//                                faceSample.getDeviceId(), remaining, currentUnFinPlaceholder.size());
                        if (remaining <= 0) {
                            currentUnFinPlaceholder.remove(faceSample.getDeviceId().toString());
//                            log.debug("[Placeholder完成] 设备 {} 的placeholder已满足并移除，剩余设备数={}",
//                                    faceSample.getDeviceId(), currentUnFinPlaceholder.size());
                        }
                    }

                    // 如果有templateId，检查是否所有placeholder都已满足
                    if (templatePlaceholder != null) {
                        if (currentUnFinPlaceholder.isEmpty()) {
                            // 使用 compareAndSet 保证原子性，避免多线程重复调用 callback
                            if (invoke.compareAndSet(false, true)) {
                                log.info("[Callback调用] 所有placeholder已满足，currentUnFinPlaceholder为空，提前调用callback");
                                task.getCallback().onInvoke();
                            }
                        }
                    }
                    if (task.faceId != null) {
                        // 经过切片后，可能有新的人脸切片生成，需要更新人脸状态
                        templateRepository.getTemplateListByScenicId(scenicId).forEach(template -> {
                            faceStatusManager.markHasNewPieces(task.faceId, template.getId());
                        });
                    }
                });
            });
        });
        try {
            executor.shutdown();
            executor.awaitTermination(3, TimeUnit.MINUTES);
            log.info("executor已结束！[A:{}/T:{}/F:{}]", executor.getActiveCount(), executor.getTaskCount(), executor.getCompletedTaskCount());
        } catch (InterruptedException e) {
            log.info("executor已中断！[A:{}/T:{}/F:{}]", executor.getActiveCount(), executor.getTaskCount(), executor.getCompletedTaskCount());
        } finally {
            executor.close();
            if (null != task.getCallback()) {
                // 使用 compareAndSet 保证原子性，避免多线程重复调用 callback
                if (invoke.compareAndSet(false, true)) {
                    log.info("[Callback调用] 兜底调用callback，currentUnFinPlaceholder剩余设备数={}",
                            currentUnFinPlaceholder.size());
                    task.getCallback().onInvoke();
                }
            }
            if (task.faceId != null) {
                faceStatusManager.setFaceCutStatus(task.faceId, FaceCutStatus.COMPLETED);
            }
        }
        if (task.getFaceId() != null) {
            FaceEntity face = faceRepository.getFace(task.getFaceId());
            if (face != null) {
                ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
                if (scenicConfig != null) {
                    // 免费送
                    List<MemberSourceEntity> sourceEntities = memberRelationRepository.listSourceByFaceRelation(face.getId(), 1);
                    if (sourceEntities.stream().noneMatch(item -> Integer.valueOf(1).equals(item.getIsFree()))) {
                        List<Long> freeSourceRelationIds = new ArrayList<>();
                        if (scenicConfig.getInteger("video_free_num") != null && scenicConfig.getInteger("video_free_num") > 0) {
                            if (scenicConfig.getInteger("video_free_num") > sourceEntities.size()) {
                                freeSourceRelationIds.addAll(sourceEntities.stream().map(MemberSourceEntity::getId).toList());
                            } else {
                                freeSourceRelationIds.addAll(sourceEntities.stream().limit(scenicConfig.getInteger("video_free_num")).map(MemberSourceEntity::getId).toList());
                            }
                        }
                        if (!freeSourceRelationIds.isEmpty()) {
                            sourceMapper.freeRelations(freeSourceRelationIds, 1);
                        }
                    }
                }
            }
        }
    }

    private boolean doCut(Long deviceId, Long faceSampleId, Date baseTime, Task task) {
        // 通过zt-device服务获取设备信息
        DeviceV2DTO deviceV2 = deviceIntegrationService.getDevice(deviceId);
        if (deviceV2 == null) {
            log.warn("设备不存在，设备ID: {}", deviceId);
            return false;
        }
        DeviceConfigManager config = deviceRepository.getDeviceConfigManager(deviceId);
        DeviceConfigEntity dConfig = deviceRepository.getDeviceConfig(deviceId);

        SourceEntity source = sourceMapper.querySameVideo(faceSampleId, deviceV2.getId());

        if (source == null || task.force) {
            BigDecimal cutPre = config.getBigDecimal("cut_pre", BigDecimal.valueOf(5L));
            BigDecimal cutPost = config.getBigDecimal("cut_post", BigDecimal.valueOf(5L));
            IDeviceStorageOperator pieceGetter = DeviceFactory.getDeviceStorageOperator(deviceV2, dConfig);
            if (pieceGetter == null) {
                return false;
            }
            BigDecimal duration = cutPre.add(cutPost);
            List<FileObject> listByDtRange = pieceGetter.getFileListByDtRange(
                    new Date(baseTime.getTime() - cutPre.multiply(BigDecimal.valueOf(1000)).longValue()),
                    new Date(baseTime.getTime() + cutPost.multiply(BigDecimal.valueOf(1000)).longValue())
            );
            if (listByDtRange.isEmpty()) {
                log.warn("没有可用的文件");
                return false;
            }
            log.info("查询到可用的文件: {}", listByDtRange);
            // 如果完全一致，就不需要裁切
            String url;
            if (listByDtRange.size() == 1 && listByDtRange.getFirst().isExact()) {
                url = listByDtRange.getFirst().getUrl();
            } else {
                long offset = baseTime.getTime() - cutPre.multiply(BigDecimal.valueOf(1000)).longValue() - listByDtRange.getFirst().getCreateTime().getTime();
                FfmpegTask ffmpegTask = new FfmpegTask();
                ffmpegTask.setFileList(listByDtRange);
                ffmpegTask.setDuration(duration);
                ffmpegTask.setOffsetStart(BigDecimal.valueOf(offset, 3));
                // 使用时间戳和线程ID确保输出文件名唯一性，避免并发冲突
                String uniqueSuffix = System.currentTimeMillis() + "_" + Thread.currentThread().threadId();
                File outFile = new File(deviceId.toString() + "_" + faceSampleId + "_" + uniqueSuffix + ".mp4");
                ffmpegTask.setOutputFile(outFile.getAbsolutePath());
                boolean result = startFfmpegTask(ffmpegTask);
                if (!result) {
                    log.warn("视频裁切失败");
                    return false;
                }
                if (outFile.length() <= 1>>20) {
                    log.warn("视频裁切文件过小，文件大小: {}kB", outFile.length() >> 10);
                    return false;
                }
                log.info("视频裁切成功");
                IStorageAdapter adapter = StorageFactory.use("assets");
                url = adapter.uploadFile("video/mp4", outFile, StorageConstant.VIDEO_PIECE_PATH, outFile.getName());
                // 上传成功后删除文件
                outFile.delete();
            }
            if (source == null) {
                SourceEntity imgSource = sourceMapper.findBySampleId(faceSampleId);
                if (imgSource == null) {
                    log.warn("imgSource为null，跳过保存source记录, faceSampleId: {}", faceSampleId);
                    return false;
                }
                SourceEntity sourceEntity = new SourceEntity();
                sourceEntity.setId(SnowFlakeUtil.getLongId());
                sourceEntity.setCreateTime(baseTime);
                if (imgSource != null) {
                    sourceEntity.setThumbUrl(imgSource.getThumbUrl());
                    sourceEntity.setUrl(imgSource.getUrl());
                    sourceEntity.setPosJson(imgSource.getPosJson());
                }
                if (StringUtils.isNotBlank(config.getString("video_crop"))) {
                    sourceEntity.setPosJson(config.getString("video_crop"));
                }
                sourceEntity.setVideoUrl(url);
                sourceEntity.setFaceSampleId(faceSampleId);
                sourceEntity.setScenicId(deviceV2.getScenicId());
                sourceEntity.setDeviceId(deviceId);
                sourceEntity.setType(1);
                
                // 先插入source记录
                sourceMapper.add(sourceEntity);

                // 然后处理关联关系
                if (task.memberId != null && task.faceId != null) {
                    List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(task.faceId, 1);
                    MemberSourceEntity videoSource = new MemberSourceEntity();
                    videoSource.setMemberId(task.getMemberId());
                    videoSource.setType(1);
                    videoSource.setFaceId(task.getFaceId());
                    videoSource.setScenicId(deviceV2.getScenicId());
                    videoSource.setSourceId(sourceEntity.getId());
                    IsBuyRespVO isBuy = orderBiz.isBuy(deviceV2.getScenicId(), task.getMemberId(), task.getFaceId(), 1, task.getFaceId());
                    if (isBuy.isBuy()) { // 如果用户买过
                        videoSource.setIsBuy(1);
                    } else if (isBuy.isFree()) { // 全免费逻辑
                        videoSource.setIsBuy(1);
                    } else {
                        videoSource.setIsBuy(0);
                    }
                    boolean anyMatch = memberSourceEntities.stream().anyMatch(memberSourceEntity -> {
                        return memberSourceEntity.getSourceId().equals(videoSource.getSourceId())
                                && memberSourceEntity.getType().equals(videoSource.getType())
                                && memberSourceEntity.getFaceId().equals(videoSource.getFaceId());
                    });
                    if (!anyMatch) {
                        // source已插入，可以直接添加关联关系
                        sourceMapper.addRelation(videoSource);
                    }
                    memberRelationRepository.clearSCacheByFace(task.faceId);
                }
            } else {
                source.setVideoUrl(url);
                if (StringUtils.isNotBlank(config.getString("video_crop"))) {
                    source.setPosJson(config.getString("video_crop"));
                }
                sourceMapper.update(source);
            }
        } else {
            // 有原视频，source已存在，可以直接添加关联关系
            if (task.memberId != null && task.faceId != null) {
                List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(task.faceId, 1);
                IsBuyRespVO isBuy = orderBiz.isBuy(deviceV2.getScenicId(), task.getMemberId(), task.getFaceId(), 1, task.getFaceId());
                MemberSourceEntity videoSource = new MemberSourceEntity();
                videoSource.setId(SnowFlakeUtil.getLongId());
                videoSource.setScenicId(deviceV2.getScenicId());
                videoSource.setFaceId(task.getFaceId());
                videoSource.setMemberId(task.getMemberId());
                videoSource.setType(1);
                if (isBuy.isBuy()) { // 如果用户买过
                    videoSource.setIsBuy(1);
                } else if (isBuy.isFree()) { // 全免费逻辑
                    videoSource.setIsBuy(1);
                } else {
                    videoSource.setIsBuy(0);
                }
                videoSource.setSourceId(source.getId());
                // 没有关联
                boolean anyMatch = memberSourceEntities.stream().anyMatch(memberSourceEntity -> {
                    return memberSourceEntity.getSourceId().equals(videoSource.getSourceId())
                            && memberSourceEntity.getType().equals(videoSource.getType())
                            && memberSourceEntity.getFaceId().equals(videoSource.getFaceId());
                });
                if (!anyMatch) {
                    // source已存在，可以直接添加关联关系
                    sourceMapper.addRelation(videoSource);
                }
                memberRelationRepository.clearSCacheByFace(task.faceId);
            }
        }
        return true;
    }

    public boolean startFfmpegTask(FfmpegTask task) {
        boolean result;
        if (task.getFileList().size() == 1) {
            // 单个文件切割，用简单方法
            result = runFfmpegForSingleFile(task);
        } else {
            // 多个文件切割，用速度快的
            result = runFfmpegForMultipleFile1(task);
        }
        return result;
    }

    private boolean runFfmpegForMultipleFile1(FfmpegTask task) {
        // 使用concat demuxer（文件列表）拼接裁切
        File concatListFile = null;
        try {
            // 创建临时文件列表
            concatListFile = File.createTempFile("concat_list_", ".txt");
            try (java.io.FileWriter writer = new java.io.FileWriter(concatListFile)) {
                for (FileObject file : task.getFileList()) {
                    // concat demuxer格式：file 'path'
                    // 对单引号进行转义
                    String escapedUrl = file.getUrl().replace("'", "'\\''");
                    writer.write("file '" + escapedUrl + "'\n");
                }
            }

            // 使用concat demuxer进行裁切
            return quickVideoCutWithConcatDemuxer(
                    concatListFile.getAbsolutePath(),
                    task.getOffsetStart(),
                    task.getDuration(),
                    task.getOutputFile()
            );
        } catch (IOException e) {
            log.error("使用concat demuxer切割视频失败", e);
            return false;
        } finally {
            // 清理临时文件
            if (concatListFile != null && concatListFile.exists()) {
                concatListFile.delete();
            }
        }
    }

    private boolean runFfmpegForMultipleFile2(FfmpegTask task) {
        // 多文件，方法二：使用计算资源编码
        try {
            return slowVideoCut(task.getFileList(), task.getOffsetStart(), task.getDuration(), task.getOutputFile());
        } catch (IOException e) {
            return false;
        }
    }

    private boolean runFfmpegForSingleFile(FfmpegTask task) {
        try {
            return quickVideoCut(task.getFileList().getFirst().getUrl(), task.getOffsetStart(), task.getDuration(), task.getOutputFile());
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * 把MP4转换成可以拼接的TS文件
     *
     * @param file        MP4文件，或ffmpeg支持的输入
     * @param outFileName 输出文件路径
     * @return 是否成功
     * @throws IOException 奇奇怪怪的报错
     */
    private boolean convertMp4ToTs(FileObject file, String outFileName) throws IOException {
        List<String> ffmpegCmd = new ArrayList<>();
        ffmpegCmd.add("ffmpeg");
        ffmpegCmd.add("-hide_banner");
        ffmpegCmd.add("-y");
        ffmpegCmd.add("-i");
        ffmpegCmd.add(file.getUrl());
        ffmpegCmd.add("-c");
        ffmpegCmd.add("copy");
        ffmpegCmd.add("-bsf:v");
        ffmpegCmd.add("h264_mp4toannexb");
        ffmpegCmd.add("-f");
        ffmpegCmd.add("mpegts");
        ffmpegCmd.add(outFileName);
        return handleFfmpegProcess(ffmpegCmd);
    }
    private boolean convertHevcToTs(FileObject file, String outFileName) throws IOException {
        List<String> ffmpegCmd = new ArrayList<>();
        ffmpegCmd.add("ffmpeg");
        ffmpegCmd.add("-hide_banner");
        ffmpegCmd.add("-y");
        ffmpegCmd.add("-i");
        ffmpegCmd.add(file.getUrl());
        ffmpegCmd.add("-c");
        ffmpegCmd.add("copy");
        ffmpegCmd.add("-bsf:v");
        ffmpegCmd.add("hevc_mp4toannexb");
        ffmpegCmd.add("-f");
        ffmpegCmd.add("mpegts");
        ffmpegCmd.add(outFileName);
        return handleFfmpegProcess(ffmpegCmd);
    }

    /**
     * 使用concat demuxer快速切割，不产生转码，速度快
     *
     * @param concatListFile concat列表文件路径
     * @param offset         离输入文件开始的偏移
     * @param length         输出文件时长
     * @param outputFile     输出文件名称
     * @return 是否成功
     * @throws IOException 奇奇怪怪的报错
     */
    private boolean quickVideoCutWithConcatDemuxer(String concatListFile, BigDecimal offset, BigDecimal length, String outputFile) throws IOException {
        List<String> ffmpegCmd = new ArrayList<>();
        ffmpegCmd.add("ffmpeg");
        ffmpegCmd.add("-hide_banner");
        ffmpegCmd.add("-y");
        ffmpegCmd.add("-f");
        ffmpegCmd.add("concat");
        ffmpegCmd.add("-safe");
        ffmpegCmd.add("0");
        ffmpegCmd.add("-probesize");
        ffmpegCmd.add(PROBE_SIZE);
        ffmpegCmd.add("-analyzeduration");
        ffmpegCmd.add("0");
        ffmpegCmd.add("-ss");
        ffmpegCmd.add(offset.toPlainString());
        ffmpegCmd.add("-i");
        ffmpegCmd.add(concatListFile);
        ffmpegCmd.add("-fflags");
        ffmpegCmd.add("+genpts");
        ffmpegCmd.add("-c:v");
        ffmpegCmd.add("copy");
        ffmpegCmd.add("-an");
        ffmpegCmd.add("-t");
        ffmpegCmd.add(length.toPlainString());
        ffmpegCmd.add("-avoid_negative_ts");
        ffmpegCmd.add("make_zero");
        ffmpegCmd.add("-f");
        ffmpegCmd.add("mp4");
        ffmpegCmd.add(outputFile);
        return handleFfmpegProcess(ffmpegCmd);
    }

    /**
     * 快速切割，不产生转码，速度快，但可能会出现：第一帧数据不是I帧导致前面的数据无法使用
     *
     * @param inputFile  输入文件，ffmpeg支持的协议均可
     * @param offset     离输入文件开始的偏移
     * @param length     输出文件时长
     * @param outputFile 输出文件名称
     * @return 是否成功
     * @throws IOException 奇奇怪怪的报错
     */
    private boolean quickVideoCut(String inputFile, BigDecimal offset, BigDecimal length, String outputFile) throws IOException {
        List<String> ffmpegCmd = new ArrayList<>();
        ffmpegCmd.add("ffmpeg");
        ffmpegCmd.add("-hide_banner");
        ffmpegCmd.add("-y");
        ffmpegCmd.add("-probesize");
        ffmpegCmd.add(PROBE_SIZE);
        ffmpegCmd.add("-analyzeduration");
        ffmpegCmd.add("0");
        ffmpegCmd.add("-ss");
        ffmpegCmd.add(offset.toPlainString());
        ffmpegCmd.add("-i");
        ffmpegCmd.add(inputFile);
        ffmpegCmd.add("-fflags");
        ffmpegCmd.add("+genpts");
        ffmpegCmd.add("-c:v");
        ffmpegCmd.add("copy");
        ffmpegCmd.add("-an");
        ffmpegCmd.add("-t");
        ffmpegCmd.add(length.toPlainString());
        ffmpegCmd.add("-avoid_negative_ts");
        ffmpegCmd.add("make_zero");
        ffmpegCmd.add("-f");
        ffmpegCmd.add("mp4");
        ffmpegCmd.add(outputFile);
        return handleFfmpegProcess(ffmpegCmd);
    }

    /**
     * 转码切割，兜底逻辑，速度慢，但优势：成功后转码视频绝对可用
     *
     * @param inputFiles 输入文件List，ffmpeg支持的协议均可
     * @param offset     离输入文件开始的偏移
     * @param length     输出文件时长
     * @param outputFile 输出文件名称
     * @return 是否成功
     * @throws IOException 奇奇怪怪的报错
     */
    private boolean slowVideoCut(List<FileObject> inputFiles, BigDecimal offset, BigDecimal length, String outputFile) throws IOException {
        List<String> ffmpegCmd = new ArrayList<>();
        ffmpegCmd.add("ffmpeg");
        ffmpegCmd.add("-hide_banner");
        ffmpegCmd.add("-y");
        for (FileObject file : inputFiles) {
            ffmpegCmd.add("-i");
            ffmpegCmd.add(file.getUrl());
        }
        // 使用filter_complex做拼接
        ffmpegCmd.add("-filter_complex");
        ffmpegCmd.add(
                IntStream.range(0, inputFiles.size()).mapToObj(i -> "[" + i + ":v]").collect(Collectors.joining("")) +
                        "concat=n="+inputFiles.size()+":v=1[v]"
        );
        ffmpegCmd.add("-map");
        ffmpegCmd.add("[v]");
        ffmpegCmd.add("-preset:v");
        ffmpegCmd.add("fast");
        ffmpegCmd.add("-fflags");
        ffmpegCmd.add("+genpts");
        ffmpegCmd.add("-an");
        // 没有使用copy，因为使用了filter_complex
        ffmpegCmd.add("-ss");
        ffmpegCmd.add(offset.toPlainString());
        ffmpegCmd.add("-t");
        ffmpegCmd.add(length.toPlainString());
        ffmpegCmd.add("-avoid_negative_ts");
        ffmpegCmd.add("make_zero");
        ffmpegCmd.add("-f");
        ffmpegCmd.add("mp4");
        ffmpegCmd.add(outputFile);

        return handleFfmpegProcess(ffmpegCmd);
    }

    /**
     * 运行ffmpeg，并确认ffmpeg是否正常退出
     *
     * @param ffmpegCmd ffmpeg命令
     * @return 是否正常退出
     */
    private static boolean handleFfmpegProcess(List<String> ffmpegCmd) throws IOException {
        Date _startDt = new Date();
        log.info("FFMPEG执行命令：【{}】", String.join(" ", ffmpegCmd));
        ProcessBuilder pb = new ProcessBuilder(ffmpegCmd);
        Process ffmpegProcess = pb.start();
        // 如果需要额外分析输出之类
        if (log.isTraceEnabled()) {
            InputStream stderr = ffmpegProcess.getErrorStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(stderr));
            String line;
            while ((line = reader.readLine()) != null) {
                log.trace(line);
            }
        }
        try {
            // 最长1分钟
            boolean exited = ffmpegProcess.waitFor(90, TimeUnit.SECONDS);
            if (exited) {
                int code = ffmpegProcess.exitValue();
                Date _endDt = new Date();
                log.info("FFMPEG执行命令结束，Code：【{}】，耗费时间：【{}ms】，命令：【{}】", code, _endDt.getTime() - _startDt.getTime(), String.join(" ", ffmpegCmd));
                return 0 == code;
            } else {
                log.error("FFMPEG执行命令没有在90秒内退出，命令：【{}】", String.join(" ", ffmpegCmd));
                ffmpegProcess.destroy();
                return false;
            }
        } catch (InterruptedException e) {
            // TODO: 被中断了
            log.warn("FFMPEG执行命令：【{}】，被中断了", String.join(" ", ffmpegCmd));
            return false;
        }
    }

    /**
     * 加载景区的设备配对关系
     * @param scenicId 景区ID
     * @return 设备配对关系Map，无配对关系时返回空Map（而非null，避免重复查询）
     */
    private Map<Long, Long> loadPairDeviceMap(Long scenicId) {
        List<DeviceV2DTO> allDeviceByScenicId = deviceRepository.getAllDeviceByScenicId(scenicId);
        if (allDeviceByScenicId == null || allDeviceByScenicId.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, Long> pairDeviceMap = new HashMap<>();
        allDeviceByScenicId.forEach(device -> {
            Long deviceId = device.getId();
            DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(deviceId);
            Long pairDevice = deviceConfig.getLong("pair_device");
            if (pairDevice != null) {
                pairDeviceMap.putIfAbsent(deviceId, pairDevice);
            }
        });
        log.debug("加载景区 {} 设备配对关系，共 {} 对", scenicId, pairDeviceMap.size());
        return pairDeviceMap.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(pairDeviceMap);
    }

}
