package com.keyware.shandan.system.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.io.FileUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.keyware.shandan.bianmu.entity.DirectoryVo;
import com.keyware.shandan.bianmu.enums.DirectoryType;
import com.keyware.shandan.bianmu.enums.ReviewStatus;
import com.keyware.shandan.bianmu.service.DirectoryService;
import com.keyware.shandan.bianmu.service.LabelEntityService;
import com.keyware.shandan.bianmu.utils.DirectoryBuilder;
import com.keyware.shandan.common.constants.DirConstant;
import com.keyware.shandan.common.entity.Result;
import com.keyware.shandan.common.service.BaseServiceImpl;
import com.keyware.shandan.common.util.DateUtil;
import com.keyware.shandan.common.util.StringUtils;
import com.keyware.shandan.common.util.UUIDUtil;
import com.keyware.shandan.frame.properties.CustomProperties;
import com.keyware.shandan.system.entity.SysFile;
import com.keyware.shandan.system.entity.SysFileChunk;
import com.keyware.shandan.system.mapper.SysFileChunkMapper;
import com.keyware.shandan.system.mapper.SysFileMapper;
import com.keyware.shandan.system.repositories.EsSysFileRepository;
import com.keyware.shandan.system.service.SysFileService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.constraints.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

/**
 * <p>
 * 系统文件表 服务实现类
 * </p>
 *
 * @author GuoXin
 * @since 2021-06-07
 */
@Slf4j
@Service
public class SysFileServiceImpl extends BaseServiceImpl<SysFileMapper, SysFile, String> implements SysFileService {
    public final static String separator = "/";
    @Autowired
    private CustomProperties customProperties;

    @Autowired
    private DirectoryService directoryService;

    @Autowired
    private SysFileChunkMapper fileChunkMapper;

    @Autowired
    private EsSysFileRepository esSysFileRepository;

    @Autowired
    private LabelEntityService labelEntityService;

    @Autowired
    private SysFileMapper fileMapper;

    @Override
    @Transactional
    public SysFile uploadFiles(MultipartFile file, SysFile sysFile) throws Exception {
        sysFile.setMultipartFile(file);
        generateFilePath(sysFile);
        file.transferTo(new File(getFileAbsolutePath(sysFile)));
        sysFile.setIsFirst(false);
        sysFile.setIsMerge(false);
        sysFile.setIsChunk(false);

        if (save(sysFile) && StringUtils.hasText(sysFile.getEntityId())) {
            DirectoryVo dir = directoryService.getById(sysFile.getEntityId());
            directoryService.saveFileDIr(sysFile, sysFile.getEntityId());

            if (dir != null && dir.getReviewStatus() != ReviewStatus.UN_SUBMIT) {
                dir.setReviewStatus(ReviewStatus.UN_SUBMIT);
                directoryService.updateById(dir);
            }
        }

        return sysFile;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public SysFile uploadFileChunk(@NotNull MultipartFile uploadFile, SysFile fileInfo, @NotNull SysFileChunk chunk) throws Exception {
        Assert.notNull(uploadFile, "上传文件不能为空");
        String fileType = FileUtil.extName(fileInfo.getFileName());
        SysFile file = getFileByMd5AndType(chunk.getFileMd5(), fileType);
        if (file == null) {
            file = new SysFile();
            // 没有查询到说明是第一次上传，先保存文件信息
            BeanUtils.copyProperties(fileInfo, file);
            // 首次上传不保存entityId，在合并文件的时候更新
            file.setEntityId("");
            file.setFileSuffix("." + fileType);
            file.setFileSize(file.getFileSize() / 1024d / 1024d);
            file.setFileType(fileType);
            file.setMD5(chunk.getFileMd5());
            file.setPath("");
            file.setIsChunk(true);
            file.setIsMerge(false);
            file.setIsFirst(true);
            saveOrUpdate(file);
        }

        //保存分片文件到临时存储目录
        if (!chunkExists(chunk)) {
            generateFileChunkPath(file, chunk);
            chunk.setChunkName(file.getFileName() + "_" + chunk.getChunkIndex());
            chunk.setFileId(file.getId());
            File transfer = new File(getFileChunkAbsolutePath(chunk));
            transfer.deleteOnExit();
            uploadFile.transferTo(transfer);

            // 合并文件
            //mergeSingleFileChunk(file, chunk);
            fileChunkMapper.insert(chunk);
            file.setCurrentChunkIndex(chunk.getChunkIndex());
            updateById(file);
        }
        return file;
    }

    public SysFile getFileByMd5AndType(String md5, String type) {
        QueryWrapper<SysFile> query = new QueryWrapper<>();
        query.eq("MD5", md5).eq(type != null, "FILE_TYPE", type).eq("IS_FIRST", true);
        return getOne(query);
    }

    @Override
    public SysFile getFileByMd5AndName(String fileMd5, String name) {
        QueryWrapper<SysFile> query = new QueryWrapper<>();
        query.eq("IS_FIRST", true).eq("MD5", fileMd5).eq(StringUtils.hasText(name), "FILE_NAME", name);
        return list(query).stream().findFirst().orElse(null);
    }

    @Override
    public SysFileChunk getFileChunkByMd5(@NotNull String fileMd5, @NotNull String chunkMd5) {
        QueryWrapper<SysFileChunk> query = new QueryWrapper<>();
        query.eq("FILE_MD5", fileMd5).eq("CHUNK_MD5", chunkMd5);
        return fileChunkMapper.selectOne(query);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result<SysFile> updateOrSave(SysFile entity) throws Exception {
        Result<SysFile> result = super.updateOrSave(entity);
        if (result.isFlag()) {
            // 保存自定义置标记录
            entity.getLabels().forEach(item -> {
                item.setEntityId(entity.getId());
            });
            labelEntityService.saveOrUpdateBatch(entity.getLabels());
        }
        return result;
    }

    @Override
    public Result<SysFile> get(String id) {
        SysFile file = getById(id);
        return Result.of(file);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result<Boolean> deleteById(String id) {
        SysFile file = getById(id);
        if (file != null) {
            labelEntityService.removeByEntityId(id);
            // 根据MD5判断是否数据库中最后一条记录，如果是，则删除,（等于2，相当于包含一条原始文件数据和非原始文件数据）
            if (!file.isChunk() || countByMd5(file.getMD5()) == 2) {
                removeByMd5(file.getMD5());
                // 删除Es中的索引
                esSysFileRepository.delete(file);
                // 删除存储的文件
                deleteFolder(getFileAbsolutePath(file));
            } else {
                // 删除数据的记录
                removeById(id);
            }
        }
        return Result.of(true);
    }

    @Override
    public void clearFiles() {
        File file = new File(customProperties.getFileStorage().getPath());
        if (file.exists()) {
            file.delete();
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public synchronized Boolean autoCreateDirAndUpdateFile(SysFile file) throws Exception {
        // 当前选中的目录
        DirectoryVo currentDir = directoryService.getById(file.getEntityId());
        Assert.notNull(currentDir, "资源目录未找到，目录ID：" + file.getEntityId());
        if (DirectoryType.LINK_DIR == currentDir.getDirectoryType()) {
            currentDir = directoryService.getById(currentDir.getResourceId());
            Assert.notNull(currentDir, "软连接的源目录未找到，目录ID：" + file.getEntityId());
        }
        // 根据文件全路径生成目录
        DirectoryVo parentDir = getOrCreateDirectoryByPath(file, currentDir);

        SysFile newFile = new SysFile();
        BeanUtil.copyProperties(file, newFile);
        newFile.setId(null);
        newFile.setIsFirst(false);
        newFile.setEntityId(parentDir.getId());
        save(newFile);

        // 保存自定义置标记录
        String newFileId = newFile.getId();
        newFile.getLabels().forEach(item -> {
            item.setEntityId(newFileId);
        });
        labelEntityService.saveOrUpdateBatch(newFile.getLabels());

        // 保存文件类型的目录
        directoryService.saveFileDIr(newFile, parentDir.getId());
        log.debug("保存文件目录数据，文件名称：{}，MD5：{}，父级目录：{}", file.getFileName(), file.getMD5(), parentDir.getId());
        return true;
    }

    private synchronized DirectoryVo getOrCreateDirectoryByPath(SysFile file, DirectoryVo currentDir) {
        // 如果有文件全路径名称，则需要根据路径自动生成目录
        if (StringUtils.hasText(file.getFileFullName())) {
            String fullPath = currentDir.getDirectoryPath() + separator + file.getFileFullName();
            String path = new File(fullPath).getParentFile().getPath();
            DirectoryVo directory = directoryService.getByPath(path);
            if (directory == null) {
                directory = createDirectoryByPath(path);
            }
            return directory;
        } else {
            return currentDir;
        }
    }

    /**
     * 根据文件路径逐级创建目录
     *
     * @param parentFullPath 文件夹路径
     * @return 创建的最后一级目录
     */
    private DirectoryVo createDirectoryByPath(String parentFullPath) {
        List<String> splitPath = getPathListByPath(parentFullPath);
        AtomicReference<DirectoryVo> directory = new AtomicReference<>(null);
        splitPath.forEach(path -> {
            DirectoryVo temp = directoryService.getByPath(path);
            if (temp == null) {
                DirectoryVo parent = directory.get();
                if (parent == null) {
                    parent = directoryService.getById(DirConstant.DIR_ROOT_ID);
                }
                temp = DirectoryBuilder.buildByParent(parent);
                temp.setDirectoryType(DirectoryType.DIRECTORY);
                temp.setDirectoryName(new File(path).getName());
                temp.setDirectoryPath(path);
                directoryService.save(temp);
            }
            directory.set(temp);
        });
        return directory.get();
    }

    /**
     * 从ES移除文件索引
     *
     * @param files 文件
     */
    @Override
    public void removeFileFromEs(List<SysFile> files) {
        if (!ObjectUtils.isEmpty(files)) {
            esSysFileRepository.deleteAll(files);
        }
    }

    @Override
    public List<SysFile> listByDirId(String dirId) {
        return fileMapper.listByDirId(dirId);
    }

    private int countByMd5(String md5) {
        QueryWrapper<SysFile> query = new QueryWrapper<>();
        query.eq("MD5", md5);
        return count(query);
    }

    private void removeByMd5(String md5) {
        QueryWrapper<SysFile> query = new QueryWrapper<>();
        query.eq("MD5", md5);
        remove(query);
    }


    /**
     * 生成文件的路径名称（包含文件名称及后缀）
     *
     * @param file 文件信息
     * @return 文件存储路径（文件夹）
     */
    private String generateFilePath(SysFile file) throws IOException {
        String path = DateUtil.getFormatNowDate("yyyy/MM/dd");
        String id = StringUtils.hasText(file.getMD5()) ? file.getMD5() : StringUtils.hasText(file.getId()) ? file.getId() : UUIDUtil.getUUID();
        String fileNewName = id + file.getFileSuffix();

        File temp = new File(customProperties.getFileStorage().getPath() + separator + path);
        if (!temp.exists()) {
            if (!temp.mkdirs()) {
                throw new IOException("目录创建失败：" + temp.getPath());
            }
        }
        file.setPath(path + separator + fileNewName);
        return path;
    }

    /**
     * 获取文件的绝对路径
     *
     * @param file 文件存储信息
     * @return 文件的绝对路径
     */
    private String getFileAbsolutePath(@NotNull SysFile file) {
        if (StringUtils.isBlank(file.getPath())) {
            return "";
        }
        return customProperties.getFileStorage().getPath() + separator + file.getPath();
    }

    /**
     * 生成文件分片的路径名称
     *
     * @param file  文件信息
     * @param chunk 文件分片信息
     * @return 文件存储路径（文件夹）
     * @throws IOException -
     */
    private String generateFileChunkPath(SysFile file, SysFileChunk chunk) throws IOException {
        String path = file.getMD5();
        File temp = new File(customProperties.getFileStorage().getTempPath() + separator + path);
        if (!temp.exists()) {
            if (!temp.mkdirs()) {
                throw new IOException("目录创建失败：" + temp.getPath());
            }
        }
        String fileName = chunk.getChunkMd5() + "_temp.chunk";
        chunk.setChunkPath(path + separator + fileName);
        return path;
    }

    /**
     * 获取文件分片的绝对路径
     *
     * @param chunk 文件分片信息
     * @return 绝对路径
     */
    private String getFileChunkAbsolutePath(@NotNull SysFileChunk chunk) {
        if (StringUtils.isBlank(chunk.getChunkPath())) {
            return "";
        }
        return customProperties.getFileStorage().getTempPath() + separator + chunk.getChunkPath();
    }

    /**
     * 删除文件分片的临时文件
     *
     * @param file 文件信息
     * @throws IOException -
     */
    private void deleteChunkFolder(@NotNull SysFile file) throws IOException {
        if (StringUtils.isNotBlank(file.getMD5())) {
            String path = generateFileChunkPath(file, new SysFileChunk());
            path = customProperties.getFileStorage().getTempPath() + separator + path;
            deleteFolder(path);
        }
    }

    /**
     * 检查chunk是否已存在
     *
     * @param chunk 文件分片信息
     * @return 是否存在
     */
    private boolean chunkExists(SysFileChunk chunk) {
        return getFileChunkByMd5(chunk.getFileMd5(), chunk.getChunkMd5()) != null;
    }

    /**
     * 删除指定路径的目录
     *
     * @param path 文件路径
     */
    private void deleteFolder(String path) {
        File file = new File(path);
        if (file.exists()) {
            File[] children = file.listFiles();
            if (file.isDirectory() && children != null) {
                for (File f : children) {
                    deleteFolder(f.getAbsolutePath());
                }
            }
            file.delete();
        }
    }

    public static List<String> getPathListByPath(String path) {
        List<String> splitPath = splitPathBySeparator(path);
        Collections.reverse(splitPath);
        List<String> pathList = new ArrayList<>();
        StringBuffer temp = new StringBuffer();
        return splitPath.stream().filter(StringUtils::hasText).map(str -> temp.append(separator).append(str).toString()).collect(Collectors.toList());
    }

    public static List<String> splitPathBySeparator(String path) {
        List<String> names = new ArrayList<>();
        if (StringUtils.hasText(path)) {
            File file = new File(path);
            names.add(file.getName());
            if (file.getParentFile() != null) {
                names.addAll(splitPathBySeparator(file.getParentFile().getPath()));
            }
        }
        return names;
    }
}
