package com.microservices.file.service.impl;

import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.utils.DateUtils;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.common.core.utils.file.FileUtils;
import com.microservices.common.core.utils.uuid.IdUtils;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.file.domain.FileUploadChunk;
import com.microservices.file.domain.FileUploadSession;
import com.microservices.file.domain.vo.CheckChunkVo;
import com.microservices.file.domain.vo.CheckInstantUploadVo;
import com.microservices.file.domain.vo.InitChunkedUploadVo;
import com.microservices.file.domain.vo.UploadProgressVo;
import com.microservices.file.domain.vo.input.InitChunkedUploadInputVo;
import com.microservices.file.mapper.FileUploadChunkMapper;
import com.microservices.file.mapper.FileUploadSessionMapper;
import com.microservices.file.service.IChunkedUploadService;
import com.microservices.file.service.IFileCommonAsyncService;
import com.microservices.file.service.ISysFileInfoService;
import com.microservices.file.utils.FileUploadUtils;
import com.microservices.file.utils.Md5Utils;
import com.microservices.system.api.domain.SysFileInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 分片上传服务实现
 *
 * @author microservices
 */
@Service
public class ChunkedUploadServiceImpl implements IChunkedUploadService {

    private static final Logger logger = LoggerFactory.getLogger(ChunkedUploadServiceImpl.class);

    @Autowired
    private FileUploadSessionMapper sessionMapper;

    @Autowired
    private FileUploadChunkMapper chunkMapper;

    @Autowired
    @Lazy
    private ISysFileInfoService fileInfoService;

    @Autowired
    @Lazy
    private IFileCommonAsyncService fileCommonAsyncService;

    /**
     * 文件存储根路径
     */
    @Value("${file.path}")
    private String filePath;

    /**
     * 最小分片大小（默认：1MB）- 安全校验边界
     */
    @Value("${chunked.upload.minChunkSize:1048576}")
    private Long minChunkSize;

    /**
     * 最大分片大小（默认：50MB）- 安全校验边界
     */
    @Value("${chunked.upload.maxChunkSize:52428800}")
    private Long maxChunkSize;

    /**
     * 会话过期时间（默认：24小时）
     */
    @Value("${chunked.upload.sessionExpireHours:24}")
    private Integer sessionExpireHours = 24;

    /**
     * 最大并发上传分片数（安全边界）
     */
    @Value("${chunked.upload.maxConcurrentChunks:10}")
    private Integer maxConcurrentChunks = 10;

    /**
     * 最大文件大小（字节，默认50GB = 1024 * 50MB）
     */
    @Value("${chunked.upload.maxFileSize:53687091200}")
    private Long maxFileSize;

    /**
     * 完整哈希文件大小阈值（小于此值使用完整文件MD5，默认100MB）
     *
     */
    @Value("${chunked.upload.fullHashThreshold:104857600}")
    private Long fullHashThreshold;

    /**
     * 哈希采样点数量
     */
    @Value("${chunked.upload.sampleCount:5}")
    private Integer sampleCount;

    @Override
    @Transactional
    public InitChunkedUploadVo initChunkedUpload(InitChunkedUploadInputVo input) {
        // 1. 参数校验
        if (StringUtils.isEmpty(input.getFileName())) {
            throw new ServiceException("文件名不能为空");
        }
        if (input.getFileSize() == null || input.getFileSize() <= 0) {
            throw new ServiceException("文件大小无效");
        }

        // 校验文件大小
        if (input.getFileSize() > maxFileSize) {
            throw new ServiceException("文件大小超过限制: %s(%d字节) ", FileUtils.calcFileSize(maxFileSize), maxFileSize);
        }

        // 2. 计算分片信息（使用服务端动态计算的分片大小）
        Long chunkSize = calculateRecommendedChunkSize(input.getFileSize());
        int totalChunks = (int) Math.ceil((double) input.getFileSize() / chunkSize);
        Integer concurrent = calculateRecommendedConcurrent(totalChunks);

        // 3. 根据文件大小判断哈希类型
        String hashType = (input.getFileSize() < fullHashThreshold) ? "FULL" : "SAMPLE";
        List<Integer> sampleChunks = null;

        if ("SAMPLE".equals(hashType)) {
            // 计算采样点
            sampleChunks = calculateSampleChunks(totalChunks, sampleCount);
        }

        // 4. 创建上传会话
        String uploadId = IdUtils.fastSimpleUUID();
        FileUploadSession session = new FileUploadSession();
        session.setUploadId(uploadId);
        session.setFileName(input.getFileName());
        session.setFileSize(input.getFileSize());
        session.setChunkSize(chunkSize);
        session.setTotalChunks(totalChunks);
        session.setUploadedChunks(0);
        session.setUploadType(input.getType());
        session.setHierarchy(input.getHierarchy());
        session.setStatus(FileUploadSession.Status.INIT.getCode());
        session.setTempFilePath(getTempSessionPath(uploadId));
        session.setCreateBy(SecurityUtils.getUsername());
        session.setCreateTime(DateUtils.getNowDate());
        session.setExpireTime(DateUtils.addHours(DateUtils.getNowDate(), sessionExpireHours));
        session.setHashType(hashType);
        if (sampleChunks != null && !sampleChunks.isEmpty()) {
            session.setSampleChunks(sampleChunks.stream().map(String::valueOf).reduce((a, b) -> a + "," + b).orElse(""));
        }

        sessionMapper.insertSession(session);

        // 5. 创建分片记录
        List<FileUploadChunk> chunks = new ArrayList<>();
        for (int i = 0; i < totalChunks; i++) {
            FileUploadChunk chunk = new FileUploadChunk();
            chunk.setUploadId(uploadId);
            chunk.setChunkNumber(i);
            chunk.setChunkSize(chunkSize);
            chunk.setStatus(FileUploadChunk.Status.PENDING.getCode());
            chunks.add(chunk);
        }
        chunkMapper.batchInsertChunks(chunks);

        // 6. 创建临时目录
        createTempDirectory(session.getTempFilePath());

        // 7. 返回结果
        InitChunkedUploadVo result = new InitChunkedUploadVo();
        result.setUploadId(uploadId);
        result.setTotalChunks(totalChunks);
        result.setChunkSize(chunkSize);
        result.setConcurrent(concurrent);
        result.setHashType(hashType);
        result.setSampleChunks(sampleChunks);

        return result;
    }

    @Override
    @Transactional
    public CheckInstantUploadVo checkInstantUpload(String uploadId, String fileSignature) {
        // 1. 校验上传会话
        FileUploadSession session = sessionMapper.selectByUploadId(uploadId);
        if (session == null) {
            throw new ServiceException("上传会话不存在");
        }

        // 2. 查询数据库是否存在相同的 file_hash 且状态为已完成
        FileUploadSession completedSession = sessionMapper.selectCompletedByFileSignatureAndType(fileSignature, session.getHashType());

        if (completedSession != null && completedSession.getFileId() != null) {
            // ========== 秒传成功 ==========
            SysFileInfo fileInfo = fileInfoService.selectSysFileInfoByFileId(completedSession.getFileId());

            if (fileInfo != null) {
                // 删除当前会话及其分片记录
                chunkMapper.deleteByUploadId(uploadId);
                sessionMapper.deleteByUploadId(uploadId);

                // 清理临时文件
                cleanupTempFiles(session);

                // 返回已存在文件信息
                return CheckInstantUploadVo.instantSuccess(
                        fileInfo.getFileId(),
                        fileInfo.getFileOriginName(),
                        fileInfo.getFilePath(),
                        fileInfo.getDownloadUrl(),
                        fileInfo.getFileSizeInfo()
                );
            }
        }
        // 3. 检查断点续传：查询数据库是否存在相同的 fileSignature 且状态为上传中或者合并中
        FileUploadSession uploadingOrMergingSession = sessionMapper.selectUploadingOrMergingByFileSignatureAndType(fileSignature, session.getHashType());
        if (uploadingOrMergingSession != null && uploadingOrMergingSession.getFileSignature() != null) {
            // ========== 断点续传成功 ==========
            // 删除当前会话及其分片记录
            chunkMapper.deleteByUploadId(uploadId);
            sessionMapper.deleteByUploadId(uploadId);
            // 清理临时文件
            cleanupTempFiles(session);
            List<Integer> completedUploadChunkedIdList = null;
            if (FileUploadSession.Status.UPLOADING.getCode().equals(uploadingOrMergingSession.getStatus())) {
                List<FileUploadChunk> chunkList = chunkMapper.selectByUploadId(uploadingOrMergingSession.getUploadId());
                completedUploadChunkedIdList = chunkList.stream()
                        .filter(x -> FileUploadChunk.Status.UPLOADED.getCode().equals(x.getStatus()))
                        .map(FileUploadChunk::getChunkNumber).collect(Collectors.toList());
            }

            // 返回已存在文件信息
            return CheckInstantUploadVo.resumingSuccess(
                    uploadingOrMergingSession.getUploadId(),
                    completedUploadChunkedIdList,
                    uploadingOrMergingSession.getStatus());
        }

        // ========== 不能秒传，存储哈希值 ==========
        session.setFileSignature(fileSignature);
        sessionMapper.updateSession(session);

        return CheckInstantUploadVo.needUpload("未找到相同文件，请继续上传分片");
    }

    @Override
    public CheckChunkVo checkChunk(String uploadId, Integer chunkNumber) {
        // 1. 校验上传会话
        FileUploadSession session = sessionMapper.selectByUploadId(uploadId);
        if (session == null) {
            throw new ServiceException("上传会话不存在");
        }

        // 2. 校验分片序号
        if (chunkNumber == null || chunkNumber < 0 || chunkNumber >= session.getTotalChunks()) {
            throw new ServiceException("分片序号无效");
        }

        // 3. 查询分片记录
        FileUploadChunk chunk = chunkMapper.selectByUploadIdAndChunkNumber(uploadId, chunkNumber);
        if (chunk == null) {
            return CheckChunkVo.notExists(chunkNumber);
        }

        // 4. 检查分片文件是否存在
        if (FileUploadChunk.Status.UPLOADED.getCode().equals(chunk.getStatus())) {
            File chunkFile = new File(chunk.getChunkPath());
            if (chunkFile.exists()) {
                return CheckChunkVo.exists(chunkNumber, chunk.getChunkSize());
            }
        }

        return CheckChunkVo.notExists(chunkNumber);
    }

    @Override
    @Transactional
    public Map<String, Object> uploadChunk(String uploadId, Integer chunkNumber, MultipartFile chunk) {
        // 1. 校验上传会话
        FileUploadSession session = sessionMapper.selectByUploadId(uploadId);
        if (session == null) {
            throw new ServiceException("上传会话不存在");
        }

        // 2. 校验分片序号
        if (chunkNumber == null || chunkNumber < 0 || chunkNumber >= session.getTotalChunks()) {
            throw new ServiceException("分片序号无效");
        }

        // 3. 校验文件
        if (chunk == null || chunk.isEmpty()) {
            throw new ServiceException("分片文件不能为空");
        }

        // 4. 更新会话状态
        if (FileUploadSession.Status.INIT.getCode().equals(session.getStatus())) {
            sessionMapper.updateStatus(uploadId, FileUploadSession.Status.UPLOADING.getCode());
        }

        // 5. 查询分片记录
        FileUploadChunk chunkRecord = chunkMapper.selectByUploadIdAndChunkNumber(uploadId, chunkNumber);
        if (chunkRecord == null) {
            throw new ServiceException("分片记录不存在");
        }

        // 6. 检查是否已上传
        if (FileUploadChunk.Status.UPLOADED.getCode().equals(chunkRecord.getStatus())) {
            File existingFile = new File(chunkRecord.getChunkPath());
            if (existingFile.exists()) {
                Map<String, Object> result = new HashMap<>();
                result.put("success", true);
                result.put("message", "分片已存在");
                result.put("chunkNumber", chunkNumber);
                return result;
            }
        }

        // 7. 保存分片文件
        String chunkFilePath = getChunkFilePath(session.getTempFilePath(), chunkNumber);
        File chunkFile = new File(chunkFilePath);

        try {
            chunk.transferTo(chunkFile);
        } catch (IOException e) {
            logger.error("保存分片文件失败: uploadId={}, chunkNumber={}", uploadId, chunkNumber, e);
            throw new ServiceException("保存分片文件失败");
        }

        // 8. 更新分片记录
        chunkRecord.setChunkPath(chunkFilePath);
        chunkRecord.setStatus(FileUploadChunk.Status.UPLOADED.getCode());
        chunkRecord.setUploadTime(DateUtils.getNowDate());
        chunkMapper.updateChunk(chunkRecord);

        // 9. 更新会话已上传分片数
        sessionMapper.incrementUploadedChunks(uploadId);

        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("message", "分片上传成功");
        result.put("chunkNumber", chunkNumber);
        return result;
    }

    @Override
    @Transactional
    public SysFileInfo mergeChunks(String uploadId) {
        // 1. 校验上传会话
        FileUploadSession session = sessionMapper.selectByUploadId(uploadId);
        if (session == null) {
            throw new ServiceException("上传会话不存在");
        }

        // 2. 幂等性：已完成的直接返回
        if (FileUploadSession.Status.COMPLETED.getCode().equals(session.getStatus())
                && session.getFileId() != null) {
            SysFileInfo fileInfo = fileInfoService.selectSysFileInfoByFileId(session.getFileId());
            if (fileInfo != null) {
                return fileInfo;
            }
        }

        // 3. 检查所有分片是否已上传
        int uploadedCount = chunkMapper.countUploadedChunks(uploadId);
        if (uploadedCount < session.getTotalChunks()) {
            throw new ServiceException("还有分片未上传，无法合并");
        }

        // 4. 状态检查：防止重复提交
        String currentStatus = session.getStatus();
        if (FileUploadSession.Status.MERGING.getCode().equals(currentStatus)) {
            // 已在合并中，直接返回 null
            return null;
        }
        if (FileUploadSession.Status.MERGE_FAILED.getCode().equals(currentStatus)) {
            throw new ServiceException("上次合并失败：" + session.getErrorMessage());
        }

        // 5. 更新状态为 MERGING
        session.setStatus(FileUploadSession.Status.MERGING.getCode());
        sessionMapper.updateSession(session);

        // 6. 异步执行合并
        fileCommonAsyncService.executeMergeAsync(uploadId);

        // 7. 立即返回
        return null;
    }

    /**
     * 合并分片文件
     */
    @Override
    public String mergeChunkFiles(String baseFilePath, FileUploadSession session) {
        String fileName = FileUploadUtils.extractFilename(session.getFileName());
        String mergedFilePath = FileUploadUtils.getAbsoluteFile(baseFilePath, fileName).getAbsolutePath();

        try (FileOutputStream fos = new FileOutputStream(mergedFilePath)) {
            List<FileUploadChunk> chunks = chunkMapper.selectByUploadId(session.getUploadId());
            for (FileUploadChunk chunk : chunks) {
                File chunkFile = new File(chunk.getChunkPath());
                if (!chunkFile.exists()) {
                    throw new ServiceException("分片文件不存在: " + chunk.getChunkPath());
                }

                try (FileInputStream fis = new FileInputStream(chunkFile)) {
                    byte[] buffer = new byte[8192];
                    int bytesRead;
                    while ((bytesRead = fis.read(buffer)) != -1) {
                        fos.write(buffer, 0, bytesRead);
                    }
                }
            }
        } catch (IOException e) {
            logger.error("合并分片文件失败: uploadId={}", session.getUploadId(), e);
            throw new ServiceException("合并分片文件失败");
        }
        // 哈希校验（FULL类型进行后置校验）
        validateFullFileHash(mergedFilePath, session);

        return FileUploadUtils.getPathFileName(fileName);
    }

    @Override
    @Transactional
    public boolean cancelUpload(String uploadId) {
        // 1. 查询会话
        FileUploadSession session = sessionMapper.selectByUploadId(uploadId);
        if (session == null) {
            return false;
        }

        // 2. 删除分片记录
        chunkMapper.deleteByUploadId(uploadId);

        // 3. 删除会话记录
        sessionMapper.deleteByUploadId(uploadId);

        // 4. 清理临时文件
        cleanupTempFiles(session);

        return true;
    }

    @Override
    public UploadProgressVo getProgress(String uploadId) {
        FileUploadSession session = sessionMapper.selectByUploadId(uploadId);
        if (session == null) {
            throw new ServiceException("上传会话不存在");
        }

        UploadProgressVo progress = new UploadProgressVo();
        progress.setUploadId(uploadId);
        progress.setFileName(session.getFileName());
        progress.setFileSize(session.getFileSize());
        progress.setTotalChunks(session.getTotalChunks());
        progress.setUploadedChunks(session.getUploadedChunks());
        progress.setStatus(session.getStatus());

        // 计算上传进度
        if (session.getTotalChunks() > 0) {
            int progressPercent = (int) ((session.getUploadedChunks() * 100) / session.getTotalChunks());
            progress.setProgress(progressPercent);
        } else {
            progress.setProgress(0);
        }

        // 合并状态下的额外信息
        if (FileUploadSession.Status.COMPLETED.getCode().equals(session.getStatus())
                && session.getFileId() != null) {
            // 获取文件信息
            SysFileInfo fileInfo = fileInfoService.selectSysFileInfoByFileId(session.getFileId());
            if (fileInfo != null) {
                progress.setFileId(fileInfo.getFileId());
                progress.setDownloadUrl(fileInfo.getDownloadUrl());
                progress.setFilePath(fileInfo.getFilePath());
            }
            progress.setMergeProgress(100);
        } else if (FileUploadSession.Status.MERGING.getCode().equals(session.getStatus())) {
            // 合并中
            progress.setMergeProgress(50);  // 简化处理，可以后续细化
        } else if (FileUploadSession.Status.MERGE_FAILED.getCode().equals(session.getStatus())) {
            // 合并失败
            progress.setErrorMessage(session.getErrorMessage());
        }

        return progress;
    }

    @Override
    @Transactional
    public int cleanExpiredSessions() {
        // 1. 查询过期会话
        List<FileUploadSession> expiredSessions = sessionMapper.selectExpiredSessions();

        if (expiredSessions.isEmpty()) {
            return 0;
        }

        logger.info("开始清理过期会话，共{}个", expiredSessions.size());

        int cleanedCount = 0;
        for (FileUploadSession session : expiredSessions) {
            try {
                // 删除分片记录
                chunkMapper.deleteByUploadId(session.getUploadId());

                // 删除会话记录
                sessionMapper.deleteByUploadId(session.getUploadId());

                // 清理临时文件
                cleanupTempFiles(session);

                cleanedCount++;
            } catch (Exception e) {
                logger.error("清理过期会话失败: uploadId={}", session.getUploadId(), e);
            }
        }

        logger.info("清理过期会话完成，成功清理{}个", cleanedCount);
        return cleanedCount;
    }


    /**
     * 创建临时目录
     */
    private void createTempDirectory(String tempPath) {
        try {
            Path path = Paths.get(tempPath);
            if (!Files.exists(path)) {
                Files.createDirectories(path);
            }
        } catch (IOException e) {
            logger.error("创建临时目录失败: {}", tempPath, e);
            throw new ServiceException("创建临时目录失败");
        }
    }

    /**
     * 清理临时文件
     */
    public void cleanupTempFiles(FileUploadSession session) {
        if (StringUtils.isNotEmpty(session.getTempFilePath())) {
            File tempDir = new File(session.getTempFilePath());
            if (tempDir.exists()) {
                deleteDirectory(tempDir);
            }
        }
    }

    /**
     * 递归删除目录
     */
    private void deleteDirectory(File directory) {
        if (directory.isDirectory()) {
            File[] files = directory.listFiles();
            if (files != null) {
                for (File file : files) {
                    deleteDirectory(file);
                }
            }
        }
        directory.delete();
    }

    /**
     * 获取临时会话路径
     */
    private String getTempSessionPath(String uploadId) {
        return filePath + "/chunks/" + uploadId + "/";
    }

    /**
     * 获取分片文件路径
     */
    private String getChunkFilePath(String tempPath, Integer chunkNumber) {
        return tempPath + "chunk_" + chunkNumber;
    }


    /**
     * 根据文件大小动态计算推荐的分片大小
     * 使用对数插值算法，基于配置的 minChunkSize 和 maxChunkSize
     *
     * @param fileSize 文件大小（字节）
     * @return 推荐的分片大小（字节）
     */
    private Long calculateRecommendedChunkSize(long fileSize) {
        long minChunk = minChunkSize;
        long maxChunk = maxChunkSize;
        long maxFile = maxFileSize;

        // 小于最小分片大小，直接返回最小值
        if (fileSize <= minChunk) {
            return minChunk;
        }

        // 对数比例：小文件敏感度高，大文件敏感度低
        double ratio = Math.log((double) fileSize / minChunk) /
                Math.log((double) maxFile / minChunk);
        ratio = Math.max(0.0, Math.min(1.0, ratio));

        // 线性插值
        long recommendedChunk = (long) (minChunk + ratio * (maxChunk - minChunk));
        recommendedChunk = Math.max(minChunk, Math.min(maxChunk, recommendedChunk));

        // 向上取整到 1MB 边界
        long mb = 1024 * 1024L;
        recommendedChunk = ((recommendedChunk + mb - 1) / mb) * mb;

        return Math.max(minChunk, Math.min(maxChunk, recommendedChunk));
    }

    /**
     * 根据分片数量动态计算推荐的并发数
     * 使用平方根缩放算法，基于配置的 maxConcurrentChunks
     *
     * @param totalChunks 分片数量
     * @return 推荐的并发数
     */
    private Integer calculateRecommendedConcurrent(int totalChunks) {
        int maxConcurrent = maxConcurrentChunks;
        int minConcurrent = 2;

        // 分片数量少时，使用最小并发
        if (totalChunks <= minConcurrent) {
            return minConcurrent;
        }

        // 平方根缩放：避免过度并发
        int recommendedConcurrent = (int) Math.ceil(Math.sqrt(totalChunks));
        recommendedConcurrent = Math.max(minConcurrent, Math.min(maxConcurrent, recommendedConcurrent));

        return recommendedConcurrent;
    }

    /**
     * 计算采样点分片序号
     *
     * @param totalChunks 总分片数
     * @param sampleCount 采样点数量
     * @return 采样点分片序号列表
     */
    private List<Integer> calculateSampleChunks(int totalChunks, int sampleCount) {
        List<Integer> samples = new ArrayList<>();
        if (totalChunks <= sampleCount) {
            for (int i = 0; i < totalChunks; i++) {
                samples.add(i);
            }
        } else {
            int interval = totalChunks / sampleCount;
            for (int i = 0; i < sampleCount; i++) {
                if (i == sampleCount - 1) {
                    samples.add(totalChunks - 1);
                } else {
                    samples.add(i * interval);
                }
            }
        }
        return samples;
    }

    /**
     * 解析采样点字符串
     *
     * @param sampleChunksStr 采样点字符串（逗号分隔）
     * @return 采样点分片序号列表
     */
    public List<Integer> parseSampleChunks(String sampleChunksStr) {
        if (StringUtils.isEmpty(sampleChunksStr)) {
            return new ArrayList<>();
        }
        return Arrays.stream(sampleChunksStr.split(","))
                .map(Integer::parseInt)
                .collect(Collectors.toList());
    }

    /**
     * 从已上传的分片中计算采样点哈希
     * <p>
     * 计算方式：
     * 1. 对采样点分片序号进行排序
     * 2. 逐个计算每个采样分片的 MD5 值
     * 3. 将所有 MD5 值拼接成一个字符串
     * 4. 计算拼接字符串的 MD5 值作为最终签名
     *
     * @param tempPath     临时文件路径
     * @param sampleChunks 采样点分片序号列表
     * @return 采样点哈希值
     */
    public String calculateSampleHashFromChunks(String tempPath, List<Integer> sampleChunks) {
        // 1. 对采样点排序
        Collections.sort(sampleChunks);

        // 2. 逐个计算每个采样分片的 MD5
        StringBuilder md5Concat = new StringBuilder();
        for (Integer chunkNumber : sampleChunks) {
            String chunkPath = getChunkFilePath(tempPath, chunkNumber);
            File chunkFile = new File(chunkPath);
            if (!chunkFile.exists()) {
                throw new ServiceException("采样分片不存在: " + chunkNumber);
            }
            String chunkMd5 = Md5Utils.calculateMd5(chunkFile);
            md5Concat.append(chunkMd5);
        }

        // 3. 计算拼接后的 MD5 值作为最终签名
        return Md5Utils.calculateMd5(md5Concat.toString().getBytes());
    }

    /**
     * 校验文件哈希
     *
     * @param filePath 合并后的文件路径
     * @param session  上传会话
     * @throws ServiceException 校验失败时抛出异常
     */
    private void validateFullFileHash(String filePath, FileUploadSession session) {
        File mergedFile = new File(filePath);
        String hashType = session.getHashType();
        String expectedHash = session.getFileSignature();

        if ("FULL".equals(hashType) || hashType == null) {
            String actualMd5 = Md5Utils.calculateMd5(mergedFile);
            if (!actualMd5.equalsIgnoreCase(expectedHash)) {
                mergedFile.delete();
                throw new ServiceException("文件MD5校验失败：期望=" + expectedHash + "，实际=" + actualMd5);
            }
        }
    }
}
