package com.ylx.massage.service.impl;

import cn.hutool.crypto.digest.DigestUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ylx.common.config.RuoYiConfig;
import com.ylx.common.core.domain.AjaxResult;
import com.ylx.common.utils.file.FileUploadUtils;
import com.ylx.common.utils.file.FileUtils;
import com.ylx.framework.config.ServerConfig;
import com.ylx.massage.domain.TbFile;
import com.ylx.massage.mapper.TbFileMapper;
import com.ylx.massage.utils.MimeTypeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.ylx.massage.service.TbFileService;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 文件管理服务实现类
 * <p>
 * 提供文件上传、文件存储、文件去重等功能的业务实现。
 * 通过 MD5 值实现文件去重，避免重复上传相同文件。
 * 继承自 MyBatis-Plus 的 ServiceImpl 类，提供基础的 CRUD 操作。
 * </p>
 *
 * @author makejava
 * @version 1.0
 * @since 2024-04-27
 */
@Service("tbFileService")
@Slf4j
public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> implements TbFileService {

    // FFmpeg 命令路径（从配置文件读取）
    @Value("${ffmpeg.path}")
    private String FFMPEG_PATH;

    /**
     * 是否启用Docker模式
     */
    @Value("${ffmpeg.docker-enabled:false}")
    private boolean dockerEnabled;
    // Docker镜像名称
    @Value("${ffmpeg.docker-image:jrottenberg/ffmpeg}")
    private String dockerImage;
    // Docker容器内工作目录
    @Value("${ffmpeg.container-work-dir:/data}")
    private String containerWorkDir;
    // 宿主机路径映射（Docker volume映射的源路径）
    @Value("${ffmpeg.host-volume-path}")
    private String hostVolumePath;
    // 截取时间点（秒）
    private static final int CAPTURE_TIME_SECOND = 1;
    // 命令执行超时时间（秒）
    private static final int COMMAND_TIMEOUT = 30;

    @Autowired
    private ServerConfig serverConfig;

    /**
     * 根据 MD5 值查询文件记录
     * <p>
     * 通过文件的 MD5 值查询数据库中是否已存在相同的文件记录。
     * 用于文件去重判断，避免重复上传相同内容的文件。
     * </p>
     *
     * @param md5 文件的 MD5 值，用于唯一标识文件内容
     * @return 如果找到匹配的文件记录则返回该记录，否则返回 null
     */
    @Override
    public TbFile getByMd5(String md5) {
        LambdaQueryWrapper<TbFile> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(TbFile::getMd5, md5);
        return this.getOne(queryWrapper);
    }

    /**
     * 计算文件的 MD5 值
     * <p>
     * 使用 Hutool 工具类计算上传文件的 MD5 哈希值。
     * MD5 值用于文件内容唯一性校验和去重判断。
     * </p>
     *
     * @param file 上传的文件对象
     * @return 文件内容的 MD5 哈希值（32位小写十六进制字符串）
     * @throws IOException 当读取文件内容发生错误时抛出
     */
    public String calculateMD5(MultipartFile file) throws IOException {
        byte[] bytes = file.getBytes();
        return DigestUtil.md5Hex(bytes);
    }

    @Override
    public AjaxResult uploadVideoFile(MultipartFile file) {
        AjaxResult ajax = AjaxResult.success();
        try {
            // 计算文件 MD5 值
            String md5 = calculateMD5(file);
            // 检查是否已存在相同文件（通过MD5去重）
            TbFile dbFile = this.getByMd5(md5);
            if (null != dbFile) {
                // 文件已存在，直接返回已有文件的访问信息（秒传）
                ajax.put("url", dbFile.getFileUrl());
                ajax.put("newFileName", FileUtils.getName(dbFile.getFileUrl()));
                ajax.put("originalFilename", dbFile.getFileName());
                // 封面图URL路径(针对视频文件)
                ajax.put("coverUrl", dbFile.getCoverUrl());
                return ajax;
            }

            // 文件不存在，上传新文件
            // 获取上传文件路径配置
            String filePath = RuoYiConfig.getUploadPath();
            // 上传文件到服务器并返回新的文件名称
            String fileName = FileUploadUtils.upload(filePath, file);
            //获取新的文件子路径
            String subFileName = fileName.substring(fileName.indexOf("/upload") + 7);
            // 构建完整的文件访问路径
            subFileName = filePath + subFileName;
            log.info("上传文件的路径：{}，返回的新文件路径：{}", filePath, subFileName);

            //生成视频的封面图片文件（该方法内部会执行 FFmpeg 命令并验证）
            String thumbnailFilePath = generateThumbnailFilePath(subFileName, file.getOriginalFilename());
            // 说明：这行代码在windows下使用，在Linux下需要注释掉
            thumbnailFilePath = thumbnailFilePath.replace("\\", "/");

            String substring = thumbnailFilePath.substring(thumbnailFilePath.lastIndexOf("/upload"));
            substring = "/profile" + substring;
            log.info("生成视频的封面图片文件路径：{}", substring);
            substring = substring.replace("\\", "/");

            ajax.put("url", fileName);
            ajax.put("newFileName", FileUtils.getName(fileName));
            ajax.put("originalFilename", file.getOriginalFilename());
            // 封面图URL路径(针对视频文件)
            ajax.put("coverUrl", substring);

            // 保存文件记录到数据库
            TbFile tbFile = new TbFile();
            tbFile.setMd5(md5);
            tbFile.setFileName(file.getOriginalFilename());
            tbFile.setFileUrl(fileName);
            tbFile.setCoverUrl(substring);
            this.save(tbFile);
            return ajax;
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.error(e.getMessage());
        }
    }

    /**
     * 上传文件并返回文件访问信息
     * <p>
     * 实现文件上传功能，支持文件去重机制：
     * 1. 计算上传文件的 MD5 值
     * 2. 检查数据库中是否已存在相同 MD5 的文件
     * 3. 如果存在，直接返回已有文件的访问路径（实现秒传）
     * 4. 如果不存在，将文件上传到服务器并保存文件记录到数据库
     * </p>
     *
     * @param file 上传的文件对象
     * @return AjaxResult 包含文件访问信息的对象：
     * - url: 文件的完整访问 URL（包含域名）
     * - fileName: 服务器存储的文件路径
     * - newFileName: 文件名（不含路径）
     * - originalFilename: 原始上传文件名
     * 如果上传失败，返回错误信息
     */
    @Override
    public AjaxResult uploadFile(MultipartFile file) {
        AjaxResult ajax = AjaxResult.success();
        try {
            // 计算文件 MD5 值
            String md5 = calculateMD5(file);
            // 检查是否已存在相同文件（通过 MD5 去重）
            TbFile dbFile = this.getByMd5(md5);
            if (null != dbFile) {
                // 文件已存在，直接返回已有文件的访问信息（秒传）
                ajax.put("url", dbFile.getFileUrl());
                ajax.put("fileName", dbFile.getFileUrl());
                ajax.put("newFileName", FileUtils.getName(dbFile.getFileUrl()));
                ajax.put("originalFilename", dbFile.getFileName());
                return ajax;
            }

            // 文件不存在，上传新文件
            // 获取上传文件路径配置
            String filePath = RuoYiConfig.getUploadPath();
            // 上传文件到服务器并返回新的文件名称
            String fileName = FileUploadUtils.upload(filePath, file);
            log.info("上传文件到服务器并返回新的文件名称：{}", fileName);
            //获取新的文件子路径
            String subFileName = fileName.substring(fileName.indexOf("/upload") + 7);
            // 构建完整的文件访问路径
            subFileName = filePath + subFileName;
            log.info("上传文件路径：{}，返回的新文件路径：{}", filePath, subFileName);
            // 构建文件访问的完整 URL
            ajax.put("url", fileName);
            ajax.put("fileName", fileName);
            ajax.put("newFileName", FileUtils.getName(fileName));
            ajax.put("originalFilename", file.getOriginalFilename());

            // 保存文件记录到数据库
            TbFile tbFile = new TbFile();
            tbFile.setMd5(md5);
            tbFile.setFileName(file.getOriginalFilename());
            tbFile.setFileUrl(fileName);
            this.save(tbFile);
            return ajax;
        } catch (Exception e) {
            // 上传失败，返回错误信息
            return AjaxResult.error(e.getMessage());
        }
    }

    /**
     * 生成封面图片文件到指定路径
     * <p>
     * 该方法会完整地执行以下操作：
     * 1. 确保保存目录存在
     * 2. 生成唯一的封面文件路径
     * 3. 使用 FFmpeg 从视频中截取封面图
     * 4. 验证生成的图片文件有效性
     * </p>
     *
     * @param videoPath        视频文件路径
     * @param originalFilename 原始视频文件名（用于生成缩略图文件名）
     * @return String 生成的封面图片文件绝对路径
     * @throws IOException                                 如果目录创建失败
     * @throws VideoThumbnailService.VideoProcessException 如果封面生成失败或验证失败
     */
    private String generateThumbnailFilePath(String videoPath, String originalFilename) throws IOException, VideoThumbnailService.VideoProcessException {
        //生成唯一的封面图片文件路径
        String baseFilename = getFileNameWithoutExtension(originalFilename);
        String thumbnailFileName = baseFilename + "_" + System.currentTimeMillis() + "_thumbnail.jpg";

        String result = videoPath.substring(0, videoPath.lastIndexOf("/") + 1);
        Path thumbnailPath = Paths.get(result, thumbnailFileName);
        String thumbnailPathStr = thumbnailPath.toString();
        log.info("开始生成封面图片，视频文件的路径: {}, 封面图片的路径: {}", videoPath, thumbnailPathStr);

        // 3. 执行 FFmpeg 命令生成封面图片
        try {
            //添加固定前缀（linux环境中需要添加，windows环境中不需要添加）
            /*String prefix = "/usr/local/java";
            executeFfmpegCommand(prefix+videoPath, prefix+thumbnailPathStr);*/
            executeFfmpegCommand(videoPath, thumbnailPathStr);
            log.info("FFmpeg命令执行成功");
        } catch (Exception e) {
            e.printStackTrace();
            throw new VideoThumbnailService.VideoProcessException("视频封面文件生成过程被中断", e);
        }

        // 4. 验证生成的封面文件
        validateThumbnailFile(thumbnailPathStr);
        log.info("视频封面图片生成成功: {}", thumbnailPathStr);
        return thumbnailPathStr;
    }

    /**
     * 将宿主机路径转换为Docker容器内路径
     * <p>
     * 例如：宿主机路径 /usr/local/java/nightFragrance/uploadPath/upload/2026/02/03/video.mp4
     * 转换为容器内路径 /data/upload/2026/02/03/video.mp4
     * </p>
     *
     * @param hostPath 宿主机上的绝对路径
     * @return Docker容器内的对应路径
     */
    private String convertToContainerPath(String hostPath) {
        if (hostPath == null || hostPath.isEmpty()) {
            return hostPath;
        }

        // 规范化路径分隔符（Windows风格转Linux风格）
        String normalizedPath = hostPath.replace("\\", "/");
        // 移除可能存在的重复斜杠
        normalizedPath = normalizedPath.replaceAll("/+", "/");

        // 获取配置的宿主机映射路径（规范化）
        String normalizedVolumePath = hostVolumePath.replace("\\", "/").replaceAll("/+", "/");
        if (!normalizedVolumePath.endsWith("/")) {
            normalizedVolumePath += "/";
        }

        // 如果路径以宿主机映射路径开头，则替换为容器工作目录
        if (normalizedPath.startsWith(normalizedVolumePath)) {
            String relativePath = normalizedPath.substring(normalizedVolumePath.length());
            return containerWorkDir + "/" + relativePath;
        }

        // 如果路径不匹配，直接返回原路径
        log.warn("路径 {} 不在Docker卷映射路径 {} 下，使用原路径", hostPath, hostVolumePath);
        return normalizedPath;
    }

    /**
     * 获取不带扩展名的文件名
     *
     * @param filename 文件名（包含扩展名）
     * @return String 不带扩展名的文件名
     */
    private String getFileNameWithoutExtension(String filename) {
        if (filename == null || filename.isEmpty()) {
            return UUID.randomUUID().toString();
        }
        int lastDotIndex = filename.lastIndexOf('.');
        return lastDotIndex > 0 ? filename.substring(0, lastDotIndex) : filename;
    }


    /**
     * 执行 FFmpeg 命令截取封面
     * <p>
     * 支持两种模式：
     * 1. 本地模式：直接调用本地FFmpeg可执行文件
     * 2. Docker模式：通过docker run命令调用容器内的FFmpeg
     * </p>
     *
     * @param videoPath     视频文件路径（宿主机路径）
     * @param thumbnailPath 封面图片保存路径（宿主机路径）
     */
    private void executeFfmpegCommand(String videoPath, String thumbnailPath) throws IOException, InterruptedException, VideoThumbnailService.VideoProcessException {
        log.info("开始执行FFmpeg命令，视频文件路径: {}, 封面图片保存路径: {}", videoPath, thumbnailPath);
        List<String> command = new ArrayList<>();
        //直接调用FFmpeg可执行文件
        command.add(FFMPEG_PATH);
        command.add("-ss");
        command.add(String.valueOf(CAPTURE_TIME_SECOND));
        command.add("-i");
        command.add(videoPath);
        command.add("-vframes");
        command.add("1");
        command.add("-q:v");
        command.add("2");
        command.add("-y");
        command.add(thumbnailPath);
        log.info("执行命令: {}", String.join(" ", command));

        // 执行命令
        ProcessBuilder processBuilder = new ProcessBuilder(command);
        // 设置工作目录
        processBuilder.directory(new File("/nightFragrance/uploadPath"));
        processBuilder.redirectErrorStream(true);
        Process process = processBuilder.start();
        // 读取命令输出（用于日志和调试）
        StringBuilder output = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }
        }
        // 等待命令执行完成（设置超时）
        boolean finished = process.waitFor(COMMAND_TIMEOUT, TimeUnit.SECONDS);
        if (!finished) {
            process.destroyForcibly();
            throw new VideoThumbnailService.VideoProcessException("FFmpeg 命令执行超时");
        }
        int exitCode = process.exitValue();
        if (exitCode != 0) {
            log.error("FFmpeg 执行失败，输出: {}", output);
            throw new VideoThumbnailService.VideoProcessException("FFmpeg 执行失败，退出码: " + exitCode);
        }
    }


    /**
     * 验证生成的封面文件
     */
    private void validateThumbnailFile(String thumbnailPath) throws VideoThumbnailService.VideoProcessException {
        File thumbnailFile = new File(thumbnailPath);
        if (!thumbnailFile.exists()) {
            throw new VideoThumbnailService.VideoProcessException("封面文件生成失败");
        }
        if (thumbnailFile.length() == 0) {
            throw new VideoThumbnailService.VideoProcessException("生成的封面文件为空");
        }
        // 可选：验证文件是否为有效的图片
        try {
            String mimeType = MimeTypeUtil.getMimeType(thumbnailFile.toPath());
            log.info("mimeType的值：{}", mimeType);
            if (mimeType == null || !mimeType.startsWith("image/")) {
                throw new VideoThumbnailService.VideoProcessException("生成的文件不是有效的图片");
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.info("无法验证文件 MIME 类型", e);
        }
    }
}

