package com.luojia_channel.modules.file.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.exception.FileException;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.file.dto.UploadChunkDTO;
import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.file.dto.CompleteUploadDTO;
import com.luojia_channel.modules.file.entity.LjFile;
import com.luojia_channel.modules.file.mapper.LjFileMapper;
import com.luojia_channel.modules.file.service.FileService;
import com.luojia_channel.modules.file.utils.GeneratePathUtil;
import com.luojia_channel.modules.file.utils.ValidateFileUtil;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.luojia_channel.modules.file.constants.FileConstant.CHUNK_BUCKET;
import static com.luojia_channel.modules.file.constants.FileConstant.CHUNK_PREFIX;


@Service
@Slf4j
@RequiredArgsConstructor
public class FileServiceImpl extends ServiceImpl<LjFileMapper, LjFile> implements FileService {

    private final MinioClient minioClient;
    private final RedisUtil redisUtil;
    private final ValidateFileUtil validateFileUtil;
    private final GeneratePathUtil generatePathUtil;
    @Value("${minio.endpoint}")
    private String endpoint;

    //线程池
    private static final ExecutorService SAVE_TO_DB_EXECUTOR = Executors.newFixedThreadPool(10);

    @PostConstruct
    private void init(){
        try {
            createBucketWithPolicy("videos");
            createBucketWithPolicy("images");
            createBucketWithPolicy("chunks");
            log.info("MinIO桶初始化完成");
        } catch (Exception e) {
            log.error("MinIO桶初始化失败: {}", e.getMessage(), e);
        }
    }

    /**
     * 创建桶并设置公共读取策略
     */
    private void createBucketWithPolicy(String name) throws Exception {
        boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(name).build());
        if (!isExist) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build());
            log.info("创建桶: {}", name);
            
            // 设置桶策略为公共读取
            String readOnlyPolicy = """
                {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {"AWS": ["*"]},
                            "Action": ["s3:GetObject"],
                            "Resource": ["arn:aws:s3:::%s/*"]
                        }
                    ]
                }
                """.formatted(name);
            
            minioClient.setBucketPolicy(
                SetBucketPolicyArgs.builder()
                    .bucket(name)
                    .config(readOnlyPolicy)
                    .build()
            );
            log.info("设置桶{}的公共读取策略成功", name);
        }
    }

    @Override
    public Boolean createBucket(String name){
        try {
            createBucketWithPolicy(name);
        } catch (Exception e) {
            log.error("创建桶失败: {}", e.getMessage(), e);
            throw new FileException("创建桶失败");
        }
        return true;
    }

    @Override
    public Boolean deleteBucket(String name){
        try {
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(name).build());
        } catch (Exception e) {
            throw new FileException("删除桶失败");
        }
        return true;
    }

    // 普通上传MultipartFile
    @Override
    public String uploadFile(MultipartFile file){
        String bucket;
        String objectName;
        String fileUrl;
        try {
            InputStream inputStream = file.getInputStream();
            String fileName = file.getOriginalFilename();
            String fileMd5 = DigestUtils.md5DigestAsHex(inputStream);
            String fileType = detectFileType(file);
            fileUrl = validateFileUtil.getExistedFileUrl(fileMd5);
            if(fileUrl != null){
                return fileUrl;
            }
            validateFileUtil.validateFile(new UploadFileDTO(file, fileType, fileMd5));
            bucket = generatePathUtil.getBucketName(fileType);
            objectName = generatePathUtil.getObjectName(fileName, fileMd5);
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucket)
                            .object(objectName)
                            .stream(file.getInputStream(),
                                    file.getSize(), -1)
                            .build()
                    );
            fileUrl = endpoint + "/" + bucket + "/" + objectName;
            /* 线程池异步化
            SAVE_TO_DB_EXECUTOR.submit(() -> saveFileToDB(fileName, fileUrl, file.getSize(),
                    fileMd5, fileType, 1));
             */
            saveFileToDB(fileName, fileUrl, file.getSize(),
                    fileMd5, fileType, 1);
        } catch (Exception e){
            log.error("文件上传失败: {}", e.getMessage());
            throw new FileException("文件上传失败");
        }
        return fileUrl;
    }

    // 普通上传
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long uploadFileAndGetFileId(UploadFileDTO fileDTO) {
        validateFileUtil.validateFile(fileDTO);
        Long fileId = validateFileUtil.getExistedFileId(fileDTO.getFileMd5());
        if(fileId != null){
            return fileId;
        }
        // 普通上传
        try {
            String fileName = fileDTO.getFile().getOriginalFilename();
            String bucket = generatePathUtil.getBucketName(fileDTO.getFileType());
            String objectName = generatePathUtil.getObjectName(fileName,
                    fileDTO.getFileMd5());
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucket)
                            .object(objectName)
                            .stream(fileDTO.getFile().getInputStream(),
                                    fileDTO.getFile().getSize(), -1)
                            .build()
            );
            return saveFileToDB(fileName, objectName, fileDTO.getFile().getSize(),
                    fileDTO.getFileMd5(), fileDTO.getFileType(), 1);
        } catch (Exception e) {
            log.error("文件上传失败: {}", e.getMessage());
            throw new FileException("文件上传失败");
        }
    }

    private Long saveFileToDB(String fileName, String fileUrl, Long fileSize,
                              String fileMd5, String fileType, Integer fileStatus){
        Long userId = UserContext.getUserId();
        LjFile file= LjFile.builder()
                .fileName(fileName)
                .fileUrl(fileUrl)
                .fileSize(fileSize)
                .fileMd5(fileMd5)
                .fileType(fileType)
                .fileStatus(fileStatus)
                .userId(userId)
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .build();
        if (!save(file)) {
            throw new FileException("文件记录保存失败");
        }
        return file.getId();
    }

    // 分片上传
    @Override
    public Boolean uploadChunk(UploadChunkDTO chunkDTO) {
        // 验证分片信息
        validateFileUtil.validateChunk(chunkDTO);
        // 判断整个文件是否存在
        if (validateFileUtil.getExistedFileId(chunkDTO.getFileMd5()) != null) {
            return true;
        }
        // 检查分片是否已上传
        if (validateFileUtil.isChunkUploaded(chunkDTO.getFileMd5(), chunkDTO.getChunkNumber())) {
            return true;
        }
        // 保存分片到MinIO临时Bucket
        String objectName = generatePathUtil
                .getChunkObjectName(chunkDTO.getFileMd5(), chunkDTO.getChunkNumber());
        // 一小时内保存进度
        String key = CHUNK_PREFIX + chunkDTO.getFileMd5();
        try {
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(CHUNK_BUCKET)
                            .object(objectName)
                            .stream(chunkDTO.getFile().getInputStream(),
                                    chunkDTO.getFile().getSize(), -1)
                            .build()
            );
            redisUtil.sAdd(key, chunkDTO.getChunkNumber());
            redisUtil.expire(key, 1, TimeUnit.HOURS);
            return true;
        } catch (Exception e) {
            // 遇到异常删除已上传的分片
            try {
                minioClient.removeObject(
                        RemoveObjectArgs.builder()
                        .bucket(CHUNK_BUCKET)
                        .object(objectName)
                        .build());
                redisUtil.sRemove(key, chunkDTO.getChunkNumber());
            } catch (Exception ex) {
                log.error("删除分片失败: {}", ex.getMessage(), ex);
            }
            log.error("分片上传失败: {}", e.getMessage());
            throw new FileException("分片上传失败");
        }
    }


    // 合并分片
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long completeUpload(CompleteUploadDTO completeDTO) {
        // 验证分片完整性
        if (!validateFileUtil.
                areAllChunksUploaded(completeDTO.getFileMd5(), completeDTO.getTotalChunks())) {
            throw new FileException("存在未上传的分片");
        }
        // 合并分片到目标Bucket
        String bucket = generatePathUtil.getBucketName(completeDTO.getFileType());
        String objectName = generatePathUtil
                .getObjectName(completeDTO.getFileName(), completeDTO.getFileMd5());
        try {
            List<ComposeSource> sources = new ArrayList<>();
            for (int i = 0; i < completeDTO.getTotalChunks(); i++) {
                String chunkObjectName = generatePathUtil
                        .getChunkObjectName(completeDTO.getFileMd5(), i);
                sources.add(
                        ComposeSource.builder()
                        .bucket(CHUNK_BUCKET)
                        .object(chunkObjectName)
                        .build());
            }
            minioClient.composeObject(
                    ComposeObjectArgs.builder()
                    .sources(sources)
                    .bucket(bucket)
                    .object(objectName)
                    .build());

            // 获得文件大小
            long fileSize = minioClient.statObject(
                    StatObjectArgs.builder()
                            .bucket(bucket)
                            .object(objectName)
                            .build()
            ).size();
            // 保存文件到数据库
            Long fileId = saveFileToDB(completeDTO.getFileName(), objectName, fileSize,
                    completeDTO.getFileMd5(), completeDTO.getFileType(), 3);
            // 删除临时分片
            deleteChunks(completeDTO.getFileMd5());
            return fileId;
        } catch (Exception e) {
            log.error("合并分片失败: {}", e.getMessage());
            throw new FileException("合并分片失败");
        }
    }

    // 删除临时分片,与遍历删除相比该操作只需要进行一次io
    public void deleteChunks(String fileMd5) {
        try {
            // 获取需要删除的对象列表
            Iterable<Result<Item>> items = minioClient.listObjects(
                    ListObjectsArgs.builder()
                            .bucket(CHUNK_BUCKET)
                            .prefix(fileMd5 + "/")
                            .build()
            );
            // 转换为 DeleteObject 列表
            List<DeleteObject> deleteObjects = new ArrayList<>();
            for (Result<Item> result : items) {
                Item item = result.get();
                log.info("要删除的分片文件：{}", item.objectName());
                deleteObjects.add(new DeleteObject(item.objectName()));
            }
            // 执行删除操作
            if (!deleteObjects.isEmpty()) {
                log.info("正在删除 {} 个分片文件", deleteObjects.size());
                Iterable<Result<DeleteError>> results = minioClient.removeObjects(
                        RemoveObjectsArgs.builder()
                                .bucket(CHUNK_BUCKET)
                                .objects(deleteObjects)
                                .build()
                );
                // 遍历结果以触发删除
                for (Result<DeleteError> result : results) {
                    DeleteError error = result.get();
                    if (error != null) {
                        log.error("删除文件 {} 失败: {}", error.objectName(), error.message());
                    }
                }
                redisUtil.delete(CHUNK_PREFIX + fileMd5);
                log.info("分片删除完成");
            } else {
                log.warn("未找到需要删除的分片文件，fileMd5={}", fileMd5);
            }
        } catch (Exception e) {
            log.error("删除分片失败: {}", e.getMessage(), e);
            throw new FileException("删除分片失败");
        }
    }

    private String detectFileType(MultipartFile file) throws Exception {
        // 获取 MIME 类型
        String mimeType = file.getContentType();
        if (mimeType == null || mimeType.isEmpty()) {
            throw new IllegalArgumentException("Could not determine file type");
        }
        // 根据 MIME 类型分类
        if (mimeType.startsWith("image/")) {
            return "image";
        } else if (mimeType.startsWith("video/")) {
            return "video";
        } else {
            throw new FileException("Unsupported file type: " + mimeType);
        }
    }

    private String getObjectUrl(MinioClient minioClient, String bucket, String objectName) throws Exception {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                .method(Method.GET)
                .bucket(bucket)
                .object(objectName)
                .build();
        return minioClient.getPresignedObjectUrl(args);
    }
}

