package ink.wgink.module.file.service.impl;

import ink.wgink.common.base.DefaultBaseService;
import ink.wgink.exceptions.FileException;
import ink.wgink.exceptions.base.SystemException;
import ink.wgink.interfaces.file.IMongoFileService;
import ink.wgink.module.file.enums.UploadTypeEnum;
import ink.wgink.module.file.pojo.dtos.FileDTO;
import ink.wgink.module.file.pojo.dtos.FileInfoDTO;
import ink.wgink.module.file.pojo.vos.FileVO;
import ink.wgink.module.file.service.IDefaultFileService;
import ink.wgink.module.file.service.IFileService;
import ink.wgink.module.file.service.IMinIoFileService;
import ink.wgink.pojo.ListPage;
import ink.wgink.pojo.pos.FilePO;
import ink.wgink.pojo.result.SuccessResultList;
import ink.wgink.properties.FileMinIoProperties;
import ink.wgink.properties.FileProperties;
import ink.wgink.util.ArrayListUtil;
import ink.wgink.util.UUIDUtil;
import ink.wgink.util.date.DateUtil;
import ink.wgink.util.request.StaticResourceRequestUtil;
import io.minio.*;
import io.minio.errors.*;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @ClassName: MinIOServiceImpl
 * @Description: minIO
 * @Author: wanggeng
 * @Date: 2021/10/17 10:16 上午
 * @Version: 1.0
 */
@Service
public class MinIoFileServiceImpl extends DefaultBaseService implements IMinIoFileService {

    @Autowired
    private FileProperties fileProperties;
    @Autowired
    private IDefaultFileService defaultFileService;
    @Autowired(required = false)
    private IMongoFileService mongoFileService;
    private FileMinIoProperties minIoProperties;
    private MinioClient minioClient;

    @PostConstruct
    public void init() {
        if (!fileProperties.getUseMinIo()) {
            return;
        }
        minIoProperties = fileProperties.getMinIo();
        minioClient = MinioClient.builder()
                .endpoint(minIoProperties.getEndpoint())
                .credentials(minIoProperties.getAccessKey(), minIoProperties.getSecretKey())
                .build();
    }

    @Override
    public String saveFileByUserId(String userId, FileVO fileVO) {
        String fileId = defaultFileService.saveFileByUserId(userId, fileVO);
        if (mongoFileService != null) {
            mongoFileService.saveByUserId(userId, fileVO);
        }
        return fileId;
    }

    @Override
    public void remove(List<String> ids) {
        if (ids.isEmpty()) {
            return;
        }
        defaultFileService.remove(ids);
        if (mongoFileService != null) {
            mongoFileService.delete(ids);
        }
    }

    @Override
    public void deleteFile(List<FilePO> filePOs) {
        // 删除数据
        if (filePOs.isEmpty()) {
            return;
        }
        List<String> fileIds = ArrayListUtil.listBeanStringIdValue(filePOs, "fileId", FilePO.class);
        defaultFileService.delete(fileIds);
        if (mongoFileService != null) {
            mongoFileService.delete(fileIds);
        }
    }

    @Override
    public void uploadFile(String token, MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params) {
        uploadFile(token, uploadFile, uploadTypeEnum, params, false);
    }

    @Override
    public void uploadFileByUserId(String userId, MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params) {
        uploadFile(userId, uploadFile, uploadTypeEnum, params, true);
    }

    @Override
    public boolean downLoadFile(HttpServletRequest request, HttpServletResponse response, Map<String, Object> params, boolean canRange) {
        String fileId = params.get("fileId").toString();
        FilePO filePO = null;
        if (mongoFileService != null) {
            filePO = mongoFileService.getPO(fileId);
        }
        if (filePO == null) {
            filePO = defaultFileService.getPO(fileId);
        }
        if (filePO == null) {
            throw new FileException("文件不存在");
        }
        boolean isOpen = Boolean.valueOf(params.get("isOpen").toString());
        return downLoadFile(request, response, filePO, isOpen, canRange);
    }

    /**
     * 上传文件
     *
     * @param tokenOrUserId
     * @param uploadFile
     * @param uploadTypeEnum
     * @param params
     * @param useUserId      是否使用外来userId
     */
    private synchronized void uploadFile(String tokenOrUserId, MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params, boolean useUserId) {
        String fileId = UUIDUtil.getUUID();
        params.put("fileId", fileId);

        String fileName = uploadFile.getOriginalFilename();
        // 文件大小
        long fileSize = uploadFile.getSize();
        // 文件类型
        String fileType = getFileType(fileName);
        String uploadFileName = fileId + "." + fileType;
        String bucketName = getBucketName(uploadTypeEnum);
        // 上传文件
        upload(uploadFileName, fileSize, bucketName, uploadFile);
        if (useUserId) {
            saveFileByUserId(tokenOrUserId, fileId, fileName, fileType, fileSize, bucketName);
        } else {
            saveFile(tokenOrUserId, fileId, fileName, fileType, fileSize, bucketName);
        }
        params.put("fileName", uploadFileName);
        params.put("fileUrl", bucketName);
        params.put("fileType", fileType);
    }

    @Override
    public boolean downLoadFile(HttpServletRequest request, HttpServletResponse response, FilePO filePO, boolean isOpen, boolean canRange) {
        boolean isDownloadComplete = false;
        // minIo中文件名
        String objectName = filePO.getFileId() + "." + filePO.getFileType();
        InputStream inputStream = null;
        try (OutputStream outputStream = response.getOutputStream();) {
            response.setHeader("Content-Length", filePO.getFileSize().toString());
            response.setContentType(StaticResourceRequestUtil.getContentType(filePO.getFileType()));
            if (!isOpen) {
                // 下载
                response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(filePO.getFileName(), "UTF-8"));
            } else {
                // 直接打开
                response.setHeader("Content-Disposition", "inline;fileName=" + URLEncoder.encode(filePO.getFileName(), "UTF-8"));
                // 如果是图片资源，开启缓存
                if (isImageFile(filePO.getFileType())) {
                    // 如果存在校验修改时间且未做修改，返回304，使用本地资源
                    String ifModifiedSince = request.getHeader("If-Modified-Since");
                    if (StringUtils.isNotBlank(ifModifiedSince) && StringUtils.equalsIgnoreCase(ifModifiedSince, filePO.getGmtModified())) {
                        response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                        return true;
                    }
                    // 缓存有效时间为7天
                    response.setHeader("Expires", DateTime.now().plusDays(7).toDateTime(DateTimeZone.forID("GMT")).toString());
                    // 一小时之内不发送新请求
                    response.setHeader("max-age", "3600");
                    response.setHeader("Last-Modified", filePO.getGmtModified());
                }
            }
            String rangeString = null;
            if (canRange && request != null) {
                rangeString = request.getHeader("Range");
                LOG.debug("range: {}", rangeString);
            }
            long contentLength = Long.valueOf(filePO.getFileSize());
            long startRange = 0;
            long endRange = contentLength - 1;
            LOG.debug("1.startRange: {}, endRange: {}", startRange, endRange);
            if (!StringUtils.isBlank(rangeString)) {
                response.setContentType("multipart/byteranges");
                String[] rangeArray = rangeString.substring(rangeString.indexOf("=") + 1).split("-");
                startRange = Long.valueOf(rangeArray[0]);
                if (rangeArray.length > 1) {
                    LOG.debug("2.startRange: {}, endRange: {}", startRange, endRange);
                    endRange = Long.valueOf(rangeArray[1]);
                }
                setRangeHeader(startRange, endRange, response, filePO.getFileId(), contentLength);
                long rangeLength = endRange - startRange + 1;
                if (rangeLength <= 0) {
                    // 标记为已完成
                    return true;
                }
                LOG.debug("rangeLength: {}", rangeLength);
                inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(filePO.getFileUrl()).object(objectName).offset(startRange).length(rangeLength).build());
            } else {
                inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(filePO.getFileUrl()).object(objectName).offset(0L).length(contentLength).build());
            }
            if (endRange >= (contentLength - 1)) {
                isDownloadComplete = true;
            }
            byte[] readBuf = new byte[IFileService.INPUT_STREAM_SIZE];
            for (int length = 0; (length = inputStream.read(readBuf)) > 0; ) {
                outputStream.write(readBuf, 0, length);
            }
            outputStream.flush();
            inputStream.close();
        } catch (ServerException | IOException | ErrorResponseException | InsufficientDataException |
                 InternalException | InvalidKeyException | InvalidResponseException | NoSuchAlgorithmException |
                 XmlParserException e) {
            throw new FileException("文件输出异常", e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return isDownloadComplete;
    }

    @Override
    public SuccessResultList<List<FileInfoDTO>> listPageInfo(ListPage page) {
        SuccessResultList<List<FilePO>> filePOSuccessResultList = null;
        if (mongoFileService != null) {
            filePOSuccessResultList = mongoFileService.listPage(page);
        }
        if (filePOSuccessResultList != null && !filePOSuccessResultList.getRows().isEmpty()) {
            List<FileInfoDTO> mongoFileInfos = filePO2FileInfo(filePOSuccessResultList.getRows());
            return new SuccessResultList<>(mongoFileInfos, filePOSuccessResultList.getPage(), filePOSuccessResultList.getTotal());
        }
        return defaultFileService.listPageInfo(page);
    }

    @Override
    public List<FileDTO> list(List<String> idList) {
        List<FilePO> filePOs = null;
        if (mongoFileService != null) {
            filePOs = mongoFileService.listByIds(idList);
        }
        if (filePOs != null && !filePOs.isEmpty()) {
            return filePO2FileDTO(filePOs);
        }
        return defaultFileService.list(idList);
    }

    @Override
    public List<FilePO> listPO(List<String> idList) {
        List<FilePO> filePOs = null;
        if (mongoFileService != null) {
            filePOs = mongoFileService.listByIds(idList);
        }
        if (filePOs != null && !filePOs.isEmpty()) {
            return filePOs;
        }
        return defaultFileService.listPO(idList);
    }

    @Override
    public FilePO getPO(String fileId) {
        FilePO filePO = null;
        if (mongoFileService != null) {
            filePO = mongoFileService.getPO(fileId);
        }
        if (filePO != null) {
            return filePO;
        }
        return defaultFileService.getPO(fileId);
    }

    @Override
    public List<FileDTO> listByUrl(String fileUrl) {
        List<FilePO> filePOs = null;
        if (mongoFileService != null) {
            filePOs = mongoFileService.listByUrl(fileUrl);
        }
        if (filePOs != null && !filePOs.isEmpty()) {
            return filePO2FileDTO(filePOs);
        }
        return defaultFileService.listByUrl(fileUrl);
    }

    /**
     * mongoFile转fileInfo
     *
     * @param filePOs
     * @return
     */
    private List<FileInfoDTO> filePO2FileInfo(List<FilePO> filePOs) {
        List<FileInfoDTO> fileInfoDTOs = new ArrayList<>();
        filePOs.forEach(mongoFile -> {
            FileInfoDTO fileInfoDTO = new FileInfoDTO();
            BeanUtils.copyProperties(mongoFile, fileInfoDTO);
            fileInfoDTOs.add(fileInfoDTO);
        });
        return fileInfoDTOs;
    }

    /**
     * mongoFile转fileDTO
     *
     * @param filePOs
     * @return
     */
    private List<FileDTO> filePO2FileDTO(List<FilePO> filePOs) {
        List<FileDTO> fileDTOs = new ArrayList<>();
        filePOs.forEach(mongoFile -> {
            FileDTO fileDTO = new FileDTO();
            BeanUtils.copyProperties(mongoFile, fileDTO);
            fileDTOs.add(fileDTO);
        });
        return fileDTOs;
    }


    /**
     * 上传文件到minIo
     *
     * @param uploadFileName
     * @param fileSize
     * @param bucketName
     * @param uploadFile
     */
    private void upload(String uploadFileName, long fileSize, String bucketName, MultipartFile uploadFile) {
        try (InputStream inputStream = uploadFile.getInputStream()) {
            // 创建桶
            if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
            minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(uploadFileName).stream(inputStream, fileSize, -1).contentType(StaticResourceRequestUtil.getContentType(uploadFileName)).build());
        } catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {
            throw new SystemException(e);
        }
    }

    /**
     * 保存文件
     *
     * @param token
     * @param fileId
     * @param fileName
     * @param fileType
     * @param fileSize
     * @param bucketName 桶名称
     */
    private void saveFile(String token, String fileId, String fileName, String fileType, long fileSize, String bucketName) {
        String userId;
        if (StringUtils.isBlank(token)) {
            userId = securityComponent.getCurrentUser().getUserId();
        } else {
            userId = getAppTokenUser(token).getId();
        }
        saveFileByUserId(userId, fileId, fileName, fileType, fileSize, bucketName);

    }

    /**
     * 保存用户
     *
     * @param userId     用户ID
     * @param fileId
     * @param fileName
     * @param fileType
     * @param fileSize
     * @param bucketName
     */
    private void saveFileByUserId(String userId, String fileId, String fileName, String fileType, long fileSize, String bucketName) {
        FileVO fileVO = getFileVO(fileId, fileName, fileType, fileSize, bucketName);
        defaultFileService.saveFile(fileVO);
        if (mongoFileService != null) {
            mongoFileService.saveByUserId(userId, fileVO);
        }
    }

    /**
     * 获取文件VO
     *
     * @param fileId
     * @param fileName
     * @param fileType
     * @param fileSize
     * @param bucketName
     * @return
     */
    private FileVO getFileVO(String fileId, String fileName, String fileType, long fileSize, String bucketName) {
        String dateTime = DateUtil.getTime();
        FileVO fileVO = new FileVO();
        fileVO.setFileId(fileId);
        fileVO.setFileName(fileName);
        fileVO.setFilePath(null);
        fileVO.setFileUrl(bucketName);
        fileVO.setFileType(fileType);
        fileVO.setFileSize(fileSize);
        fileVO.setFileSummary("minio");
        fileVO.setIsBack(1);
        fileVO.setGmtCreate(dateTime);
        fileVO.setGmtModified(dateTime);
        return fileVO;
    }

    /**
     * 获取文件类型
     *
     * @param fileName
     * @return
     */
    private String getFileType(String fileName) {
        String[] names = fileName.split("\\.");
        if (names != null) {
            return names[names.length - 1].toLowerCase();
        }
        return "";
    }

    /**
     * 获取桶文件名
     *
     * @param uploadTypeEnum 上传类型
     * @return
     */
    private String getBucketName(UploadTypeEnum uploadTypeEnum) {
        if (uploadTypeEnum.getValue() == UploadTypeEnum.IMAGE.getValue()) {
            return "images";
        } else if (uploadTypeEnum.getValue() == UploadTypeEnum.VIDEO.getValue()) {
            return "videos";
        } else if (uploadTypeEnum.getValue() == UploadTypeEnum.AUDIO.getValue()) {
            return "audios";
        }
        return "files";
    }

    /**
     * 是否是图片
     *
     * @param fileType
     * @return
     */
    private boolean isImageFile(String fileType) {
        if (StringUtils.equalsIgnoreCase("gif", fileType)) {
            return false;
        }
        String imageTypes = fileProperties.getImageTypes();
        for (String imageType : imageTypes.split(",")) {
            if (StringUtils.equalsIgnoreCase(fileType, imageType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 处理视频流问题
     *
     * @param startRange
     * @param endRange
     * @param response
     * @param fileId
     * @param contentLength
     */
    private void setRangeHeader(long startRange, long endRange, HttpServletResponse response, String fileId, long contentLength) {
        // 这里不设置，会出现第一次加载很慢的情况
        response.setHeader("Content-Length", String.valueOf(endRange - startRange + 1));
        response.setHeader("Content-Range", String.format("bytes %d-%d/%d", startRange, endRange, contentLength));
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("Etag", fileId);
        response.setStatus(206);
    }
}
