package com.hotwj.platform.video.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import com.hotwj.platform.video.config.VideoUploadProperties;
import com.hotwj.platform.video.domain.bo.VideoChunkUploadBo;
import com.hotwj.platform.video.domain.bo.VideoMergeBo;
import com.hotwj.platform.video.domain.vo.VideoTranscodeTaskInfo;
import com.hotwj.platform.video.service.VideoChunkUploadService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.service.ISysOssService;
import org.redisson.api.RSet;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 视频分片上传、合并与异步转码服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class VideoChunkUploadServiceImpl implements VideoChunkUploadService {

    private static final String CHUNK_KEY_PREFIX = "video:chunk:";
    private static final String TASK_KEY_PREFIX = "video:task:";
    private static final String RESULT_KEY_PREFIX = "video:result:";

    private final VideoUploadProperties properties;
    private final ISysOssService ossService;

    @Override
    public List<Integer> checkUploadedChunks(String fileHash) {
        return getUploadedChunkIndexes(fileHash);
    }

    @Override
    public void uploadChunk(VideoChunkUploadBo bo) {
        validateChunkRequest(bo);
        File chunkDir = getChunkDir(bo.getFileHash());
        FileUtil.mkdir(chunkDir);
        File chunkFile = new File(chunkDir, bo.getChunkIndex().toString());
        try {
            bo.getChunk().transferTo(chunkFile);
        } catch (IOException e) {
            throw new ServiceException("分片上传失败");
        }
        RSet<Integer> uploadedSet = RedisUtils.getClient().getSet(CHUNK_KEY_PREFIX + bo.getFileHash());
        uploadedSet.add(bo.getChunkIndex());
        uploadedSet.expire(properties.chunkExpireDuration());
    }

    @Override
    public Map<String, String> mergeAndCreateTask(VideoMergeBo bo) {
        validateMergeRequest(bo);
        validateFfmpegAvailability();

        String cachedM3u8Url = getResolvedM3u8Url(bo.getFileHash());
        if (StringUtils.isNotBlank(cachedM3u8Url)) {
            return Collections.singletonMap("taskId", saveCompletedTask(bo.getFileHash(), bo.getFileName(), cachedM3u8Url));
        }

        List<Integer> uploadedChunkIndexes = getUploadedChunkIndexes(bo.getFileHash());
        Set<Integer> uploaded = new HashSet<>(uploadedChunkIndexes);
        for (int i = 0; i < bo.getTotalChunks(); i++) {
            if (!uploaded.contains(i)) {
                throw new ServiceException("存在未上传完成的分片，无法合并");
            }
        }

        File sourceDir = getSourceDir(bo.getFileHash());
        FileUtil.mkdir(sourceDir);
        File mergedFile = new File(sourceDir, sanitizeFileName(bo.getFileName()));
        mergeChunksToFile(bo.getFileHash(), bo.getTotalChunks(), mergedFile.toPath());

        String taskId = "task_" + IdUtil.fastSimpleUUID();
        VideoTranscodeTaskInfo taskInfo = new VideoTranscodeTaskInfo()
            .setTaskId(taskId)
            .setFileHash(bo.getFileHash())
            .setFileName(bo.getFileName())
            .setStatus("waiting")
            .setCreateTime(LocalDateTime.now())
            .setUpdateTime(LocalDateTime.now());
        saveTask(taskInfo);
        SpringUtils.getAopProxy(this).startTranscodeAsync(taskId, bo.getFileHash(), mergedFile.toPath());
        return Collections.singletonMap("taskId", taskId);
    }

    @Override
    public VideoTranscodeTaskInfo getTaskInfo(String taskId) {
        VideoTranscodeTaskInfo taskInfo = RedisUtils.getCacheObject(TASK_KEY_PREFIX + taskId);
        if (taskInfo == null) {
            throw new ServiceException("任务不存在或已过期");
        }
        if ("completed".equals(taskInfo.getStatus()) && StringUtils.isNotBlank(taskInfo.getFileHash())) {
            String resolvedM3u8Url = getResolvedM3u8Url(taskInfo.getFileHash());
            if (StringUtils.isNotBlank(resolvedM3u8Url) && !StringUtils.equals(taskInfo.getM3u8Url(), resolvedM3u8Url)) {
                taskInfo.setM3u8Url(resolvedM3u8Url).setUpdateTime(LocalDateTime.now());
                saveTask(taskInfo);
            }
        }
        return taskInfo;
    }

    @Async
    public void startTranscodeAsync(String taskId, String fileHash, Path sourceFile) {
        VideoTranscodeTaskInfo taskInfo = getTaskInfo(taskId);
        try {
            taskInfo.setStatus("processing").setUpdateTime(LocalDateTime.now()).setErrorMessage(null);
            saveTask(taskInfo);

            Path outputDir = getHlsDir(fileHash).toPath();
            FileUtil.mkdir(outputDir.toFile());
            cleanDirectory(outputDir);

            Path playlistPath = outputDir.resolve("index.m3u8");
            runFfmpeg(sourceFile, outputDir, playlistPath);
            String m3u8Url = uploadHlsDirectoryToOss(fileHash, outputDir);

            taskInfo.setStatus("completed")
                .setM3u8Url(m3u8Url)
                .setUpdateTime(LocalDateTime.now())
                .setErrorMessage(null);
            saveTask(taskInfo);
            RedisUtils.setCacheObject(RESULT_KEY_PREFIX + fileHash, m3u8Url, properties.taskExpireDuration());

            FileUtil.del(getChunkDir(fileHash));
            FileUtil.del(getSourceDir(fileHash));
            FileUtil.del(outputDir.toFile());
            log.info("视频转码完成: taskId={}, fileHash={}, playlist={}", taskId, fileHash, playlistPath);
        } catch (Exception e) {
            log.error("视频转码失败: taskId={}, fileHash={}, msg={}", taskId, fileHash, e.getMessage(), e);
            taskInfo.setStatus("failed")
                .setErrorMessage(StringUtils.substring(e.getMessage(), 0, 500))
                .setUpdateTime(LocalDateTime.now());
            saveTask(taskInfo);
        }
    }

    private void validateChunkRequest(VideoChunkUploadBo bo) {
        if (bo.getChunkIndex() < 0) {
            throw new ServiceException("分片索引不能小于0");
        }
        if (bo.getTotalChunks() <= 0) {
            throw new ServiceException("总分片数必须大于0");
        }
        if (bo.getChunkIndex() >= bo.getTotalChunks()) {
            throw new ServiceException("分片索引超出范围");
        }
    }

    private void validateMergeRequest(VideoMergeBo bo) {
        if (bo.getTotalChunks() <= 0) {
            throw new ServiceException("总分片数必须大于0");
        }
    }

    private void validateFfmpegAvailability() {
        String ffmpegCommand = StringUtils.defaultIfBlank(properties.getFfmpegCommand(), "ffmpeg").trim();
        Process process = null;
        try {
            process = new ProcessBuilder(ffmpegCommand, "-version")
                .redirectErrorStream(true)
                .start();
            boolean finished = process.waitFor(5, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                throw new ServiceException("FFmpeg 校验超时，请检查命令配置或容器环境");
            }
            String output;
            try (InputStream inputStream = process.getInputStream()) {
                output = new String(inputStream.readAllBytes());
            }
            if (process.exitValue() != 0) {
                throw new ServiceException("FFmpeg 不可用: " + abbreviate(output, 300));
            }
        } catch (IOException e) {
            throw new ServiceException("FFmpeg 不可用，请检查命令配置: " + ffmpegCommand);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new ServiceException("FFmpeg 校验被中断");
        }
    }

    private void mergeChunksToFile(String fileHash, int totalChunks, Path targetFile) {
        try {
            Files.deleteIfExists(targetFile);
            Files.createDirectories(targetFile.getParent());
            try (OutputStream outputStream = Files.newOutputStream(targetFile)) {
                for (int i = 0; i < totalChunks; i++) {
                    Path chunkPath = getChunkDir(fileHash).toPath().resolve(String.valueOf(i));
                    if (!Files.exists(chunkPath)) {
                        throw new ServiceException("缺少分片: " + i);
                    }
                    try (InputStream inputStream = Files.newInputStream(chunkPath)) {
                        inputStream.transferTo(outputStream);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            throw new ServiceException("合并分片失败");
        }
    }

    private void runFfmpeg(Path sourceFile, Path outputDir, Path playlistPath) throws IOException, InterruptedException {
        List<String> command = new ArrayList<>();
        command.add(properties.getFfmpegCommand());
        command.add("-y");
        command.add("-i");
        command.add(sourceFile.toAbsolutePath().toString());
        command.add("-profile:v");
        command.add("baseline");
        command.add("-level");
        command.add("3.0");
        command.add("-start_number");
        command.add("0");
        command.add("-hls_time");
        command.add(String.valueOf(properties.getHlsSegmentSeconds() == null ? 10 : properties.getHlsSegmentSeconds()));
        command.add("-hls_list_size");
        command.add("0");
        command.add("-hls_segment_filename");
        command.add(outputDir.resolve("segment_%05d.ts").toAbsolutePath().toString());
        command.add("-f");
        command.add("hls");
        command.add(playlistPath.toAbsolutePath().toString());

        ProcessBuilder processBuilder = new ProcessBuilder(command);
        processBuilder.redirectErrorStream(true);
        Process process = processBuilder.start();
        String output;
        try (InputStream inputStream = process.getInputStream()) {
            output = new String(inputStream.readAllBytes());
        }
        int exitCode = process.waitFor();
        if (exitCode != 0 || !Files.exists(playlistPath)) {
            throw new ServiceException("FFmpeg 转码失败: " + abbreviate(output, 1000));
        }
    }

    private void saveTask(VideoTranscodeTaskInfo taskInfo) {
        RedisUtils.setCacheObject(TASK_KEY_PREFIX + taskInfo.getTaskId(), taskInfo, properties.taskExpireDuration());
    }

    private List<Integer> getUploadedChunkIndexes(String fileHash) {
        File chunkDir = getChunkDir(fileHash);
        if (!chunkDir.exists() || !chunkDir.isDirectory()) {
            return Collections.emptyList();
        }
        File[] chunkFiles = chunkDir.listFiles();
        if (chunkFiles == null || chunkFiles.length == 0) {
            return Collections.emptyList();
        }
        List<Integer> result = new ArrayList<>();
        for (File chunkFile : chunkFiles) {
            if (chunkFile == null || !chunkFile.isFile()) {
                continue;
            }
            String fileName = chunkFile.getName();
            if (!fileName.matches("\\d+")) {
                continue;
            }
            result.add(Integer.parseInt(fileName));
        }
        result.sort(Integer::compareTo);
        return result;
    }

    private String getResolvedM3u8Url(String fileHash) {
        String cachedM3u8Url = RedisUtils.getCacheObject(RESULT_KEY_PREFIX + fileHash);
        if (isOssUrl(cachedM3u8Url)) {
            return cachedM3u8Url;
        }

        Path outputDir = getHlsDir(fileHash).toPath();
        Path playlistPath = outputDir.resolve("index.m3u8");
        if (Files.exists(playlistPath)) {
            try {
                String uploadedM3u8Url = uploadHlsDirectoryToOss(fileHash, outputDir);
                RedisUtils.setCacheObject(RESULT_KEY_PREFIX + fileHash, uploadedM3u8Url, properties.taskExpireDuration());
                return uploadedM3u8Url;
            } catch (IOException e) {
                throw new ServiceException("上传 HLS 到 OSS 失败");
            }
        }
        return null;
    }

    private String saveCompletedTask(String fileHash, String fileName, String m3u8Url) {
        String taskId = "task_" + IdUtil.fastSimpleUUID();
        VideoTranscodeTaskInfo taskInfo = new VideoTranscodeTaskInfo()
            .setTaskId(taskId)
            .setFileHash(fileHash)
            .setFileName(fileName)
            .setStatus("completed")
            .setM3u8Url(m3u8Url)
            .setCreateTime(LocalDateTime.now())
            .setUpdateTime(LocalDateTime.now());
        saveTask(taskInfo);
        return taskId;
    }

    private String uploadHlsDirectoryToOss(String fileHash, Path outputDir) throws IOException {
        if (!Files.exists(outputDir)) {
            throw new ServiceException("HLS 输出目录不存在");
        }
        List<Path> files;
        try (var stream = Files.list(outputDir)) {
            files = stream
                .filter(Files::isRegularFile)
                .sorted(Comparator.comparing(path -> path.getFileName().toString()))
                .toList();
        }
        if (files.isEmpty()) {
            throw new ServiceException("未找到待上传的 HLS 文件");
        }

        Path playlistPath = outputDir.resolve("index.m3u8");
        if (!Files.exists(playlistPath)) {
            throw new ServiceException("未生成 index.m3u8");
        }

        for (Path path : files) {
            if (path.equals(playlistPath)) {
                continue;
            }
            ossService.upload(path.toFile(), buildOssObjectKey(fileHash, path.getFileName().toString()), resolveContentType(path.getFileName().toString()));
        }

        SysOssVo playlistOss = ossService.upload(
            playlistPath.toFile(),
            buildOssObjectKey(fileHash, playlistPath.getFileName().toString()),
            resolveContentType(playlistPath.getFileName().toString())
        );
        return playlistOss.getUrl();
    }

    private boolean isOssUrl(String url) {
        return StringUtils.isNotBlank(url) && (url.startsWith("http://") || url.startsWith("https://"));
    }

    private String buildOssObjectKey(String fileHash, String fileName) {
        String prefix = properties.normalizedPublicUrlPrefix();
        if (prefix.startsWith("/")) {
            prefix = prefix.substring(1);
        }
        return prefix + "/" + fileHash + "/" + fileName;
    }

    private String resolveContentType(String fileName) {
        String lowerFileName = fileName == null ? "" : fileName.toLowerCase(Locale.ROOT);
        if (lowerFileName.endsWith(".m3u8")) {
            return "application/vnd.apple.mpegurl";
        }
        if (lowerFileName.endsWith(".ts")) {
            return "video/mp2t";
        }
        return "application/octet-stream";
    }

    private void cleanDirectory(Path directory) throws IOException {
        if (!Files.exists(directory)) {
            return;
        }
        try (var stream = Files.list(directory)) {
            stream.forEach(path -> FileUtil.del(path.toFile()));
        }
    }

    private File getChunkDir(String fileHash) {
        return new File(properties.getWorkDir(), "chunks" + File.separator + fileHash);
    }

    private File getSourceDir(String fileHash) {
        return new File(properties.getWorkDir(), "source" + File.separator + fileHash);
    }

    private File getHlsDir(String fileHash) {
        return new File(properties.getWorkDir(), "hls" + File.separator + fileHash);
    }

    private String sanitizeFileName(String fileName) {
        return fileName.replace("\\", "_").replace("/", "_");
    }

    private String abbreviate(String text, int maxLength) {
        if (text == null || text.length() <= maxLength) {
            return text;
        }
        return text.substring(0, maxLength);
    }
}
