package com.yingpaipay.business.service.impl;

import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.yingpaipay.business.constant.*;
import com.yingpaipay.business.domain.DocumentAuditLog;
import com.yingpaipay.business.domain.Folder;
import com.yingpaipay.business.domain.Project;
import com.yingpaipay.business.domain.bo.*;
import com.yingpaipay.business.domain.excel.ProjectExcel;
import com.yingpaipay.business.domain.vo.*;
import com.yingpaipay.business.enumeration.DocumentStatusEnum;
import com.yingpaipay.business.enumeration.MailTemplateEnum;
import com.yingpaipay.business.mapper.DocumentAuditLogMapper;
import com.yingpaipay.common.file.util.PdfUtils;
import com.yingpaipay.system.mapper.SysUserFoldersMapper;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.io.FilenameUtils;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.exception.BusinessException;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mail.utils.MailUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.oss.core.OssClient;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.service.ISysDictTypeService;
import org.dromara.system.service.ISysOssService;
import org.dromara.system.service.ISysUserService;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import com.yingpaipay.business.domain.Document;
import com.yingpaipay.business.mapper.DocumentMapper;
import com.yingpaipay.business.service.IDocumentService;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * 文档Service业务层处理
 *
 * @author Huanyi
 * @date 2025-12-11
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class DocumentServiceImpl implements IDocumentService {

    private final DocumentMapper baseMapper;
    private final DocumentAuditLogMapper auditLogMapper;
    private final SysUserFoldersMapper userFoldersMapper;

    private final ISysDictTypeService dictTypeService;
    private final ISysOssService ossService;
    private final ISysUserService userService;
    private final CommonFolderService folderService;
    private final CommonProjectService projectService;

    /**
     * 水印的锁
     */
    private static final Map<Long, Object> WATERMARK_LOCK = new ConcurrentHashMap<>();

    /**
     * 查询文档
     *
     * @param id 主键
     * @return 文档
     */
    @Override
    public DocumentVo queryById(Long id) {
        return baseMapper.selectVoById(id);
    }

    /**
     * 分页查询文档列表
     *
     * @param bo        查询条件
     * @param pageQuery 分页参数
     * @return 文档分页列表
     */
    @Override
    public TableDataInfo<DocumentVo> queryPageList(DocumentBo bo, PageQuery pageQuery) {
        LambdaQueryWrapper<Document> lqw = buildQueryWrapper(bo);
        Page<DocumentVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
        dealVo(result.getRecords());
        return TableDataInfo.build(result);
    }

    private void dealVo(List<DocumentVo> documents) {
        List<Long> ossIds = new ArrayList<>();
        Map<Long, SysOssVo> ossMap = new HashMap<>();
        List<Long> folderIds = new ArrayList<>();
        Map<Long, String> folderMap = new HashMap<>();
        List<Long> userIds = new ArrayList<>();
        Map<Long, String> userMap = new HashMap<>();
        documents.forEach(e -> {
            ossIds.add(e.getActualDocument());
            folderIds.add(e.getFolderId());
            userIds.add(e.getSubmitter());
            userIds.add(e.getPlanSubmitter());
        });
        folderService.queryListByIds(folderIds).forEach(e -> folderMap.put(e.getId(), e.getName()));
        ossService.queryListByIds(ossIds).forEach(e -> ossMap.put(e.getOssId(), e));
        userService.selectUserByIds(userIds).forEach(e -> userMap.put(e.getUserId(), e.getNickName()));
        documents.forEach(e -> {
            SysOssVo ossVo = ossMap.get(e.getActualDocument());
            if (ossVo != null) {
                e.setActualDocumentName(ossVo.getOriginalName());
            }
            e.setSubmitterName(userMap.get(e.getSubmitter()));
            e.setPlanSubmitterName(userMap.get(e.getPlanSubmitter()));
            String folderName = folderMap.get(e.getFolderId());
            e.setFolderName(StringUtils.isNotBlank(folderName) ? folderName : MessageUtils.message("search.temp"));
        });
    }

    /**
     * 查询符合条件的文档列表
     *
     * @param bo 查询条件
     * @return 文档列表
     */
    @Override
    public List<DocumentVo> queryList(DocumentBo bo) {
        LambdaQueryWrapper<Document> lqw = buildQueryWrapper(bo);
        List<DocumentVo> vos = baseMapper.selectVoList(lqw);
        dealVo(vos);
        return vos;
    }

    private LambdaQueryWrapper<Document> buildQueryWrapper(DocumentBo bo) {
        Map<String, Object> params = bo.getParams();
        LambdaQueryWrapper<Document> lqw = Wrappers.lambdaQuery();
        lqw.orderByDesc(Document::getId);
        lqw.eq(bo.getFolderId() != null, Document::getFolderId, bo.getFolderId());
        lqw.eq(StringUtils.isNotBlank(bo.getSpecification()), Document::getSpecification, bo.getSpecification());
        lqw.like(StringUtils.isNotBlank(bo.getName()), Document::getName, bo.getName());
        lqw.eq(bo.getStatus() != null, Document::getStatus, bo.getStatus());
        lqw.between(params.get("beginSubmitDeadline") != null && params.get("endSubmitDeadline") != null,
            Document::getSubmitDeadline, params.get("beginSubmitDeadline"), params.get("endSubmitDeadline"));
        lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null,
            Document::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime"));
        lqw.between(params.get("beginUpdateTime") != null && params.get("endUpdateTime") != null,
            Document::getUpdateTime, params.get("beginUpdateTime"), params.get("endUpdateTime"));

        // 如果为待归档,那么就只能看到自己的上传的
        if (bo.getFolderId().equals(FolderTypeConst.UN_UPLOAD)) {
            lqw.eq(Document::getCreateBy, LoginHelper.getUserId());
        }

        return lqw;
    }

    /**
     * 新增文档
     *
     * @param bo 文档
     * @return 是否新增成功
     */
    @Override
    public Boolean insertByBo(DocumentBo bo) {
        Document add = MapstructUtils.convert(bo, Document.class);

        if (bo.getType().equals(DocumentTypeConst.PLAN)) {

            SysUserVo user = userService.getById(bo.getPlanSubmitter());
            MailTemplateEnum template = MailTemplateEnum.TO_SUBMIT;
            MailUtils.sendHtml(
                user.getEmail(),
                template.getTitle(),
                MessageFormat.format(
                    template.getContent(),
                    user.getNickName(),
                    bo.getName(),
                    projectService.queryById(bo.getProjectId()).getName(),
                    folderService.getPathById(bo.getFolderId(), bo.getProjectId()),
                    DateUtils.formatDateTime(bo.getSubmitDeadline())
                )
            );
        }

        add.setSpecificationType(folderService.getSpecificationType(bo.getFolderId(), bo.getProjectId()));

        validEntityBeforeSave(add);
        boolean flag = baseMapper.insert(add) > 0;
        if (flag) {
            bo.setId(add.getId());
        }
        return flag;
    }

    /**
     * 修改文档
     *
     * @param bo 文档
     * @return 是否修改成功
     */
    @Override
    public Boolean updateByBo(DocumentBo bo) {
        Document update = MapstructUtils.convert(bo, Document.class);
        validEntityBeforeSave(update);
        return baseMapper.updateById(update) > 0;
    }

    /**
     * 保存前的数据校验
     */
    private void validEntityBeforeSave(Document entity) {
        //TODO 做一些数据校验,如唯一约束
    }

    /**
     * 校验并批量删除文档信息
     *
     * @param ids     待删除的主键集合
     * @param isValid 是否进行有效性校验
     * @return 是否删除成功
     */
    @Override
    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
        if (isValid) {
            //TODO 做一些业务上的校验,判断是否需要校验
        }
        return baseMapper.deleteByIds(ids) > 0;
    }

    @Override
    public boolean mark(DocumentMarkBo bo) {
        return baseMapper.update(
            Wrappers.lambdaUpdate(Document.class)
                .eq(Document::getId, bo.getId())
                .set(Document::getSpecification, bo.getType())
        ) > 0;
    }

    @Override
    public boolean submit(DocumentSubmitBo bo) {

        MailTemplateEnum template = MailTemplateEnum.TO_AUDIT;
        Document document = baseMapper.selectById(bo.getDocumentId());
        SysUserVo auditor = userService.getById(document.getCreateBy());
        MailUtils.sendHtml(
            auditor.getEmail(),
            template.getTitle(),
            MessageFormat.format(
                template.getContent(),
                auditor.getNickName(),
                document.getName(),
                projectService.queryById(document.getProjectId()).getName(),
                folderService.getPathById(document.getFolderId(), document.getProjectId()),
                DateUtils.formatDateTime(new Date())
                )
        );

        document.setActualDocument(bo.getOssId());
        document.setSubmitTime(new Date());
        document.setEffectiveDate(bo.getEffectiveDate());
        document.setStatus(DocumentStatusEnum.UN_AUDIT.getValue());
        return baseMapper.updateById(document) > 0;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean audit(DocumentAuditBo bo) {

        Document document = baseMapper.selectById(bo.getDocumentId());
        if (document == null) {
            throw new BusinessException(MessageUtils.message("document.document.audit.documentnotfound"));
        }

        SysUserVo user = userService.getById(document.getPlanSubmitter() != null ? document.getPlanSubmitter() : document.getSubmitter());

        if (bo.getResult().equals(DocumentStatusEnum.AUDIT_REJECT.getValue())) {
            MailTemplateEnum template = MailTemplateEnum.AUDIT_REJECT;
            MailUtils.sendHtml(
                user.getEmail(),
                template.getTitle(),
                MessageFormat.format(
                    template.getContent(),
                    user.getNickName(),
                    document.getName(),
                    projectService.queryById(document.getProjectId()).getName(),
                    folderService.getPathById(document.getFolderId(), document.getProjectId()),
                    DateUtils.formatDateTime(new Date()),
                    bo.getRejectReason()
                )
            );
        } else {
            MailTemplateEnum template = MailTemplateEnum.AUDIT_PASS;
            MailUtils.sendHtml(
                user.getEmail(),
                template.getTitle(),
                MessageFormat.format(
                    template.getContent(),
                    user.getNickName(),
                    document.getName(),
                    projectService.queryById(document.getProjectId()).getName(),
                    folderService.getPathById(document.getFolderId(), document.getProjectId()),
                    DateUtils.formatDateTime(new Date())
                )
            );
        }

        if (Objects.equals(bo.getResult(), DocumentStatusEnum.UN_ARCHIEVED.getValue())) {
            document.setPassTime(new Date());
        }
        document.setStatus(bo.getResult());
        boolean documentFlag = baseMapper.updateById(document) == 0;
        if (documentFlag) {
            throw new RuntimeException("修改文档状态失败");
        }

        boolean logFlag = auditLogMapper.insert(buildLog(document, DocumentAuditorTypeConst.MANUAL, bo)) == 0;
        if (logFlag) {
            throw new RuntimeException("新增审核日志失败");
        }

        return true;
    }

    @Override
    public boolean confirmSubmit(Long id) {
        return baseMapper.deleteById(id) > 0;
    }

    @Override
    public TableDataInfo<DocumentAuditLogVo> logAudit(DocumentAuditLogBo bo, PageQuery pageQuery) {
        if (bo.getDocumentId() == null) {
            return TableDataInfo.build();
        }
        IPage<DocumentAuditLogVo> page = auditLogMapper.selectVoPage(
            pageQuery.build(),
            Wrappers.lambdaQuery(DocumentAuditLog.class)
                .orderByDesc(DocumentAuditLog::getId)
                .eq(DocumentAuditLog::getDocumentId, bo.getDocumentId())
                .eq(bo.getResult() != null, DocumentAuditLog::getResult, bo.getResult())
                .ge(bo.getParams().containsKey("earliestTime"), DocumentAuditLog::getAuditTime, bo.getParams().get("earliestTime"))
                .le(bo.getParams().containsKey("lagtestTime"), DocumentAuditLog::getAuditTime, bo.getParams().get("lagtestTime"))
        );
        buildLogVos(page.getRecords());
        return TableDataInfo.build(page);
    }

    private void buildLogVos(List<DocumentAuditLogVo> vos) {
        List<Long> documentIds = new ArrayList<>();
        Map<Long, Document> documentMap = new HashMap<>();
        List<Long> ossIds = new ArrayList<>();
        Map<Long, SysOssVo> ossMap = new HashMap<>();
        vos.forEach(e -> {
            documentIds.add(e.getDocumentId());
            ossIds.add(e.getUploadVersion());
        });
        ossService.queryListByIds(ossIds).forEach(e -> ossMap.put(e.getOssId(), e));
        if (!documentIds.isEmpty()) {
            baseMapper.selectByIds(documentIds).forEach(e -> documentMap.put(e.getId(), e));
        }
        vos.forEach(e -> {
            SysOssVo ossVo = ossMap.get(e.getUploadVersion());
            Document document = documentMap.get(e.getDocumentId());
            e.setUploadVersionUrl(Optional.ofNullable(ossVo).map(SysOssVo::getUrl).orElse(null));
            e.setDocumentName(Optional.ofNullable(document).map(Document::getName).orElse(null));
        });
    }

    @Override
    public boolean filing(DocumentFilingBo bo) {
        return baseMapper.update(
            Wrappers.lambdaUpdate(Document.class)
                .eq(Document::getId, bo.getDocumentId())
                .set(Document::getStatus, DocumentStatusEnum.ARCHIEVED.getValue())
        ) > 0;
    }

    @Override
    public TableDataInfo<TaskCenterSubmissionListVo> listOnSubmission(TaskCenterSubmissionListBo bo, PageQuery pageQuery) {
        List<Long> projectIds = projectService.queryIds(bo.getProjectCode(), bo.getProjectName());
        if (projectIds.isEmpty()) {
            return TableDataInfo.build();
        }
        List<Long> folderIds = folderService.selectIds(bo.getCenterName(), projectIds);
        LambdaQueryWrapper<Document> wrapper = buildSubmissionListWrapper(bo, folderIds);
        IPage<Document> page = baseMapper.selectPage(pageQuery.build(), wrapper);
        return TableDataInfo.build(page.convert(e -> {
            TaskCenterSubmissionListVo vo = new TaskCenterSubmissionListVo();
            vo.setId(e.getId());
            vo.setName(e.getName());
            vo.setStatus(e.getStatus());
            vo.setType(e.getType());
            vo.setDocumentType(e.getPlanType());
            vo.setDeadline(e.getSubmitDeadline());
            vo.setSubmitTime(e.getSubmitTime());
            vo.setCreateTime(e.getCreateTime());
            vo.setSendFlag(e.getSendFlag());
            vo.setSendStatus(e.getSendStatus());
            vo.setPlanSubmitter(e.getPlanSubmitter());
            return vo;
        }));
    }

    /**
     * FIXME 必须得是自己有这个审核权限,才能看到这个文件的审核
     */
    @Override
    public TableDataInfo<TaskCenterAuditListVo> listOnAudit(TaskCenterAuditListBo bo, PageQuery pageQuery) {
        List<Long> projectIds = projectService.queryIds(bo.getProjectCode(), bo.getProjectName());
        if (projectIds.isEmpty()) {
            return TableDataInfo.build();
        }
        List<Long> folderIds = folderService.selectIds(bo.getCenterName(), projectIds);
        LambdaQueryWrapper<Document> wrapper = buildAuditListWrapper(bo, folderIds);
        IPage<Document> page = baseMapper.selectPage(pageQuery.build(), wrapper);
        List<Long> ossIds = new ArrayList<>();
        Map<Long, SysOssVo> ossMap = new HashMap<>();
        page.getRecords().forEach(e -> ossIds.add(e.getActualDocument()));
        ossService.queryListByIds(ossIds).forEach(e -> ossMap.put(e.getOssId(), e));
        return TableDataInfo.build(page.convert(e -> {
            TaskCenterAuditListVo vo = new TaskCenterAuditListVo();
            vo.setId(e.getId());
            vo.setName(e.getName());
            vo.setStatus(e.getStatus());
            vo.setDeadline(e.getSubmitDeadline());
            vo.setSubmitter(e.getSubmitter());
            vo.setSubmitTime(e.getSubmitTime());
            vo.setCreateTime(e.getCreateTime());
            vo.setActualDocument(e.getActualDocument());
            vo.setFileName(ossMap.get(e.getActualDocument()).getOriginalName());
            vo.setActualDocumentUrl(ossMap.get(e.getActualDocument()).getUrl());
            vo.setType(e.getType());
            vo.setDocumentType(e.getPlanType());
            return vo;
        }));
    }

    @Override
    public void download(Long ossId, HttpServletResponse response) throws IOException {
        SysOssVo sysOss = ossService.getById(ossId);
        if (sysOss == null) {
            throw new BusinessException(MessageUtils.message("document.document.download.notfound"));
        }

        String originalExt = FilenameUtils.getExtension(sysOss.getOriginalName());
        File tempFile = File.createTempFile("oss_download_", "." + originalExt);
        tempFile.deleteOnExit();

        try {
            OssClient storage = OssFactory.instance(sysOss.getService());
            try (FileOutputStream fos = new FileOutputStream(tempFile)) {
                storage.download(sysOss.getFileName(), fos, null);
            }

            File processedFile = PdfUtils.watermark(tempFile, buildRemark(ossId));

            FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
            response.setContentLengthLong(processedFile.length());

            try (FileInputStream fis = new FileInputStream(processedFile);
                 BufferedInputStream bis = new BufferedInputStream(fis);
                 OutputStream os = response.getOutputStream()) {

                byte[] buffer = new byte[8192];
                int len;
                while ((len = bis.read(buffer)) != -1) {
                    os.write(buffer, 0, len);
                }
                os.flush();
            }

        } finally {
            if (tempFile.exists()) {
                tempFile.delete();
            }
        }
    }

    private String buildRemark(Long ossId) {
        Document document = baseMapper.selectOne(Wrappers.lambdaQuery(Document.class).eq(Document::getActualDocument, ossId));
        // 也有可能直接下载审核版本
        if (document == null) {
            DocumentAuditLog log = auditLogMapper.selectOne(
                Wrappers.lambdaQuery(DocumentAuditLog.class)
                    .eq(DocumentAuditLog::getUploadVersion, ossId)
            );
            if (log == null) {
                throw new BusinessException(MessageUtils.message("document.document.download.notfound"));
            }
            document = baseMapper.selectById(log.getDocumentId());
        }

        Object lock = WATERMARK_LOCK.computeIfAbsent(document.getId(), id -> new Object());
        String key = GlobalConstants.DOCUMENT_WATERMARK_KEY + document.getId();
        synchronized (lock) {
            int count;
            if (RedisUtils.hasKey(key)) {
                count = RedisUtils.getCacheObject(key);
                count++;
            } else {
                count = 1;
            }
            RedisUtils.setCacheObject(key, count);
            return document.getId() + ":" + String.format("%06d", count);
        }
    }

    @Override
    public TableDataInfo<DocumentVo> listOnSearch(DocumentSearchBo bo, PageQuery pageQuery) {
        List<Long> projectIds = new ArrayList<>();
        projectService.queryOnSearch(bo.getProjectName(), bo.getProjectCode())
            .forEach(e -> projectIds.add(e.getId()));
        IPage<DocumentVo> page = baseMapper.selectVoPage(pageQuery.build(), buildOnSearchWrapper(bo, projectIds));
        dealVo(page.getRecords());
        return TableDataInfo.build(page);
    }

    @Override
    public boolean send(DocumentSendBo bo) {
        return baseMapper.update(
            Wrappers.lambdaUpdate(Document.class)
                .eq(Document::getId, bo.getId())
                .set(Document::getSendStatus, true)
        ) > 0;
    }

    @Override
    public TableDataInfo<TaskCenterFilingListVo> listOnFiling(TaskCenterFilingListBo bo, PageQuery pageQuery) {
        List<Long> projectIds = projectService.queryIdsOnFiling(bo.getProjectCode(), bo.getProjectName());
        if (projectIds.isEmpty()) {
            return TableDataInfo.build();
        }
        List<Long> folderIds = folderService.selectIdsOnFiling(bo.getCenterName(), projectIds);
        LambdaQueryWrapper<Document> wrapper = buildFilingListWrapper(bo, folderIds);
        IPage<Document> page = baseMapper.selectPage(pageQuery.build(), wrapper);
        List<Long> ossIds = new ArrayList<>();
        Map<Long, String> ossMap = new HashMap<>();
        List<Long> folderIdsInDocument = new ArrayList<>();
        Map<Long, String> folderMap = new HashMap<>();
        page.getRecords().forEach(e -> {
            ossIds.add(e.getActualDocument());
            folderIdsInDocument.add(e.getFolderId());
        });
        folderService.listByIds(folderIdsInDocument).forEach(e -> folderMap.put(e.getId(), e.getName()));
        ossService.queryListByIds(ossIds).forEach(e -> ossMap.put(e.getOssId(), e.getUrl()));
        return TableDataInfo.build(page.convert(e -> {
            TaskCenterFilingListVo vo = new TaskCenterFilingListVo();
            vo.setId(e.getId());
            vo.setName(e.getName());
            vo.setStatus(e.getStatus());
            vo.setSubmitter(e.getSubmitter());
            vo.setSubmitTime(e.getSubmitTime());
            vo.setCreateTime(e.getCreateTime());
            vo.setOssId(e.getActualDocument());
            vo.setOssUrl(ossMap.get(e.getActualDocument()));
            vo.setType(e.getType());
            vo.setDocumentType(e.getPlanType());
            vo.setProjectId(e.getProjectId());
            vo.setFolderId(e.getFolderId());
            vo.setFolderName(folderMap.get(e.getFolderId()));
            vo.setPassTime(e.getPassTime());
            return vo;
        }));
    }

    @Override
    public AppletMineCountVo getCount() {
        AppletMineCountVo vo = new AppletMineCountVo();
        List<Document> documentList = baseMapper.selectList(
            Wrappers.lambdaQuery(Document.class)
                .eq(Document::getPlanSubmitter, LoginHelper.getUserId())
                .in(Document::getStatus, List.of(DocumentStatusEnum.UN_UPLOAD.getValue(), DocumentStatusEnum.UN_AUDIT.getValue(), DocumentStatusEnum.AUDIT_REJECT.getValue()))
        );
        vo.setToSubmit(
            documentList
                .stream()
                .filter(e ->
                    e.getStatus().equals(DocumentStatusEnum.UN_UPLOAD.getValue()) || e.getStatus().equals(DocumentStatusEnum.AUDIT_REJECT.getValue())
                ).toList().size()
        );
        vo.setToAudit(
            documentList
                .stream()
                .filter(e ->
                    e.getStatus().equals(DocumentStatusEnum.UN_AUDIT.getValue())
                ).toList().size()
        );
        return vo;
    }

    @Override
    public TableDataInfo<AppletRecentDocumentVo> listRecentOnApplet(AppletRecentDocumentBo bo, PageQuery pageQuery) {
        IPage<Document> page = baseMapper.selectPage(
            pageQuery.build(),
            Wrappers.lambdaQuery(Document.class)
                .like(StringUtils.isNotBlank(bo.getName()), Document::getName, bo.getName())
                .eq(Document::getSubmitter, LoginHelper.getUserId())
                .ne(Document::getStatus, DocumentStatusEnum.UN_UPLOAD.getValue())
                .orderByDesc(Document::getId)
        );
        return TableDataInfo.build(page.convert(e -> {
            AppletRecentDocumentVo vo = new AppletRecentDocumentVo();
            vo.setId(e.getId());
            vo.setName(e.getName());
            vo.setCreateTime(e.getSubmitTime());
            vo.setOssId(e.getActualDocument());
            return vo;
        }));
    }

    @Override
    public TableDataInfo<AppletMyTaskDocumentVo> listDocument(AppletMyTaskDocumentBo bo, PageQuery pageQuery) {
        IPage<Document> page = baseMapper.selectPage(
            pageQuery.build(),
            Wrappers.lambdaQuery(Document.class)
                .eq(Document::getPlanSubmitter, LoginHelper.getUserId())
                .eq(bo.getStatus() != null, Document::getStatus, bo.getStatus())
                .like(StringUtils.isNotBlank(bo.getName()), Document::getName, bo.getName())
                .orderByDesc(Document::getId)
        );
        return TableDataInfo.build(page.convert(e -> {
            AppletMyTaskDocumentVo vo = new AppletMyTaskDocumentVo();
            vo.setId(e.getId());
            vo.setName(e.getName());
            vo.setCreateTime(e.getCreateTime());
            vo.setOssId(e.getActualDocument());
            vo.setCreateBy(e.getCreateBy());
            vo.setSubmitter(e.getSubmitter());
            vo.setStatus(e.getStatus());
            return vo;
        }));
    }

    @Override
    public TableDataInfo<AppletDocumentScanSubmitVo> listToSubmit(AppletDocumentScanSubmitBo bo, PageQuery pageQuery) {
        IPage<Document> page = baseMapper.selectPage(
            pageQuery.build(),
            Wrappers.lambdaQuery(Document.class)
                .eq(Document::getPlanSubmitter, LoginHelper.getUserId())
                .in(Document::getStatus, List.of(DocumentStatusEnum.UN_UPLOAD.getValue(), DocumentStatusEnum.AUDIT_REJECT.getValue()))
                .like(StringUtils.isNotBlank(bo.getName()), Document::getName, bo.getName())
                .orderByDesc(Document::getId)
        );
        List<Long> projectIds = new ArrayList<>();
        Map<Long, Folder> folderMap = new HashMap<>();
        Map<Long, String> projectMap = new HashMap<>();

        page.getRecords().forEach(e -> {
            if (!projectIds.contains(e.getProjectId())) {
                projectIds.add(e.getProjectId());
            }
        });
        projectService.queryByIds(projectIds).forEach(e -> projectMap.put(e.getId(), e.getName()));
        List<Folder> folderList = folderService.queryByProjectIds(projectIds);
//        folderList.forEach(e -> folderMap.put(e.getId(), e));
        for (Folder folder : folderList) {
            folderMap.put(folder.getId(), folder);
        }

        return TableDataInfo.build(page.convert(e -> {
            AppletDocumentScanSubmitVo vo = new AppletDocumentScanSubmitVo();
            vo.setId(e.getId());
            vo.setName(e.getName());

            // 显示全路径
            Folder currentFolder = folderMap.get(e.getFolderId());
            String folder = "";
            while (currentFolder != null) {
                folder = "/" + currentFolder.getName() + folder;
                currentFolder = folderMap.get(currentFolder.getParentId());
            }
            vo.setFolder(folder);

            vo.setProject(projectMap.get(e.getProjectId()));
            vo.setCreateBy(e.getCreateBy());
            vo.setCreateTime(e.getCreateTime());
            vo.setDeadline(e.getSubmitDeadline());
            return vo;
        }));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean uploadOnSubmit(AppletUploadOnSubmitBo bo) {

        Document document = baseMapper.selectById(bo.getDocumentId());

        List<String> fileBase64List = bo.getFileBase64List();

        if (fileBase64List == null || fileBase64List.isEmpty()) {
            throw new BusinessException(MessageUtils.message("document.document.uploadempty"));
        }

        File file;
        try {
            file = convertToFile(fileBase64List, document.getName());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        SysOssVo ossVo = ossService.upload(file);

        return baseMapper.update(
            Wrappers.lambdaUpdate(Document.class)
                .eq(Document::getId, bo.getDocumentId())
                .set(Document::getActualDocument, ossVo.getOssId())
                .set(Document::getStatus, DocumentStatusEnum.UN_AUDIT.getValue())
                .set(Document::getSubmitter, LoginHelper.getUserId())
                .set(Document::getSubmitTime, new Date())
                .set(Document::getEffectiveDate, bo.getEffectiveDate())
        ) > 0;
    }

    private File convertToFile(List<String> fileBase64List, String fileName) throws IOException {

        List<File> files = new ArrayList<>();

        File finalFile;

        for (int i = 0; i < fileBase64List.size(); i++) {
            String base64Str = fileBase64List.get(i);
            if (base64Str == null || base64Str.trim().isEmpty()) {
                continue;
            }

            String cleanBase64 = base64Str;
            if (base64Str.contains(",")) {
                cleanBase64 = base64Str.split(",", 2)[1];
            }

            byte[] pdfBytes = Base64.getDecoder().decode(cleanBase64);

            Path tempPath = Files.createTempFile(IdUtil.fastSimpleUUID(), ".pdf");
            Files.write(tempPath, pdfBytes);
            files.add(tempPath.toFile());
        }

        if (files.isEmpty()) {
            throw new BusinessException(MessageUtils.message("document.document.cannotparseusablepdf"));
        }

        finalFile = PdfUtils.merge(files, fileName);

        return finalFile;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean uploadNew(AppletUploadNewBo bo) {

        List<String> fileBase64List = bo.getFiles();

        if (fileBase64List == null || fileBase64List.isEmpty()) {
            throw new BusinessException(MessageUtils.message("document.document.uploadempty"));
        }

        File file;
        try {
            file = convertToFile(fileBase64List, bo.getFileName());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        SysOssVo ossVo = ossService.upload(file);

        Document document = new Document();
        document.setFolderId(bo.getFolderId());
        document.setName(bo.getFileName());
        document.setStatus(DocumentStatusEnum.UN_AUDIT.getValue());
        document.setActualDocument(ossVo.getOssId());
        document.setNote(bo.getNote());
        document.setType(DocumentTypeConst.NOT_PLAN);
        document.setSubmitter(LoginHelper.getUserId());
        document.setSubmitTime(new Date());
        document.setProjectId(bo.getProjectId());
        document.setSendFlag(false);
        document.setSendStatus(false);
        document.setTenantId(TenantHelper.getTenantId());

        return baseMapper.insert(document) > 0;
    }

    @Override
    public AppletGetRejectionVo getRejection(Long documentId) {
        return new AppletGetRejectionVo(
            auditLogMapper.selectOne(
                Wrappers.lambdaQuery(DocumentAuditLog.class)
                    .eq(DocumentAuditLog::getDocumentId, documentId)
                    .orderByDesc(DocumentAuditLog::getId)
                    .last("LIMIT 1")
            ).getRejectReason()
        );
    }

    /**
     * 文件上传和插入数据库并非原子操作,因此无需使用事务控制
     */
    @Override
    public boolean upload(AppletUploadBo bo) {

        List<String> fileBase64List = bo.getFiles();

        if (fileBase64List == null || fileBase64List.isEmpty()) {
            throw new BusinessException(MessageUtils.message("document.document.uploadempty"));
        }

        File file;
        try {
            file = convertToFile(fileBase64List, bo.getName());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        SysOssVo ossVo = ossService.upload(file);

        List<Folder> folders = folderService.queryByProjectIds(List.of(bo.getProjectId()));
        Map<Long, Folder> folderMap = new HashMap<>();
            folders.forEach(e -> folderMap.put(e.getId(), e));
        Map<Long, List<Folder>> childrenMap = folders.stream().filter(e -> e.getParentId() != null).collect(Collectors.groupingBy(Folder::getParentId));
        List<Long> selected = new ArrayList<>();
        if (bo.getCenter() != 0L) {
            buildFolders(selected, folderMap.get(bo.getCenter()), childrenMap);
        } else {
            if (bo.getCountry() != 0L) {
                buildFolders(selected, folderMap.get(bo.getCountry()), childrenMap);
            } else {
                selected = folders.stream().map(Folder::getId).toList();
            }
        }
        List<Document> documents = baseMapper.selectList(
            Wrappers.lambdaQuery(Document.class)
                .in(Document::getFolderId, selected)
        );
        for (Document document : documents) {
            if (document.getName().equals(bo.getName())) {
                document.setActualDocument(ossVo.getOssId());
                document.setSubmitter(LoginHelper.getUserId());
                document.setSubmitTime(new Date());
                document.setStatus(DocumentStatusEnum.UN_AUDIT.getValue());
                document.setEffectiveDate(bo.getEffectiveDate());
                return baseMapper.updateById(document) > 0;
            }
        }

        Document document = new Document();
        document.setFolderId(bo.getCenter() != 0L ? bo.getCenter() : bo.getCountry());
        document.setName(bo.getName());
        document.setStatus(DocumentStatusEnum.UN_AUDIT.getValue());
        document.setActualDocument(ossVo.getOssId());
        document.setType(DocumentTypeConst.NOT_PLAN);
        document.setSubmitter(LoginHelper.getUserId());
        document.setSubmitTime(new Date());
        document.setProjectId(bo.getProjectId());
        document.setEffectiveDate(bo.getEffectiveDate());
        document.setSpecificationType(bo.getCenter() != 0L ? SpecificationTypeConst.CENTER : SpecificationTypeConst.PROJECT);
        document.setTenantId(TenantHelper.getTenantId());

        return baseMapper.insert(document) > 0;
    }

    @Override
    public long countTemp(Long projectId) {
        return baseMapper.selectCount(
            Wrappers.lambdaQuery(Document.class)
                .eq(Document::getProjectId, projectId)
                .eq(Document::getFolderId, 0)
        );
    }

    @Override
    public boolean removeTemp(Long documentId) {
        return baseMapper.deleteById(documentId) > 0;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean specify(DocumentSpecifyBo bo) {

        Document originDocument = baseMapper.selectById(bo.getDocumentId());

        if (bo.getMissingDocumentId() != null) {
            Document document = baseMapper.selectById(bo.getMissingDocumentId());
            document.setActualDocument(originDocument.getActualDocument());
            document.setSubmitter(originDocument.getSubmitter());
            document.setSubmitTime(originDocument.getSubmitTime());
            document.setStatus(DocumentStatusEnum.UN_AUDIT.getValue());
            boolean updateFlag = baseMapper.updateById(document) == 0;
            if (updateFlag) {
                throw new RuntimeException("修改目标文件失败");
            }
            boolean deleteFlag = baseMapper.deleteById(originDocument) == 0;
            if (deleteFlag) {
                throw new RuntimeException("删除临时文件失败");
            }
            return true;
        } else {
            if (bo.getFolderId() == null) {
                throw new BusinessException(MessageUtils.message("document.document.temp.specify.nowhere"));
            }
            originDocument.setFolderId(bo.getFolderId());
            return baseMapper.updateById(originDocument) > 0;
        }

    }

    @Override
    public TableDataInfo<DocumentListOnSpecifyVo> listOnSpecify(DocumentListOnSpecifyBo bo, PageQuery pageQuery) {
        List<Folder> folders = folderService.queryByProjectIds(List.of(bo.getProjectId()));
        Map<Long, Folder> folderMap = new HashMap<>();
        List<Long> folderIds = new ArrayList<>();
        folders.forEach(e -> {
            folderIds.add(e.getId());
            folderMap.put(e.getId(), e);
        });
        IPage<Document> page = baseMapper.selectPage(pageQuery.build(), buildSpecifyListWrapper(bo, folderIds));
        return TableDataInfo.build(page.convert(e -> {
            DocumentListOnSpecifyVo vo = new DocumentListOnSpecifyVo();
            vo.setId(e.getId());
            vo.setName(e.getName());
            StringBuilder folder = new StringBuilder();
            long current = e.getFolderId();
            for (;;) {
                Folder currentFolder = folderMap.get(current);
                folder.insert(0, currentFolder.getName()).insert(0, "/");
                if (currentFolder.getParentId() == null) {
                    break;
                }
                current = currentFolder.getParentId();
            }
            vo.setFolder(folder.toString());
            vo.setStatus(e.getStatus());
            vo.setDeadline(e.getSubmitDeadline());
            vo.setPlanSubmitter(e.getPlanSubmitter());
            vo.setCreateBy(e.getCreateBy());
            vo.setCreateTime(e.getCreateTime());
            return vo;
        }));
    }

    @Override
    public List<ProjectExcel> getListByProjectId(Long projectId) {
        List<Document> documentList = baseMapper.selectList(
            Wrappers.lambdaQuery(Document.class)
                .eq(Document::getProjectId, projectId)
        );
        List<Long> ossIds = new ArrayList<>();
        List<Long> userIds = new ArrayList<>();
        Map<Long, SysOssVo> ossMap = new HashMap<>();
        Map<Long, SysUser> userMap = new HashMap<>();
        Map<String, String> planType = new HashMap<>();
        documentList.forEach(e -> {
            ossIds.add(e.getActualDocument());
            userIds.add(e.getPlanSubmitter());
            userIds.add(e.getCreateBy());
            userIds.add(e.getSubmitter());
        });
        ossService.queryListByIds(ossIds).forEach(e -> ossMap.put(e.getOssId(), e));
        userService.selectUserByIds(userIds).forEach(e -> userMap.put(e.getUserId(), e));
        dictTypeService.selectDictDataByType(DictTypeConst.PLAN_DOCUMENT_TYPE).forEach(e -> planType.put(e.getDictValue(), e.getDictLabel()));
        Project project = projectService.queryById(projectId);
        List<Folder> folders = folderService.queryByProjectId(projectId);
        Map<Long, Folder> folderMap = new HashMap<>();
        folders.forEach(e -> folderMap.put(e.getId(), e));
        List<ProjectExcel> list = new ArrayList<>();
        for (Document document : documentList) {
            ProjectExcel excel = new ProjectExcel();
            excel.setId(document.getId());
            excel.setName(document.getName());
            excel.setStatus(DocumentStatusEnum.getLabel(document.getStatus()));
            excel.setActualDocumentName(
                Optional.ofNullable(document.getActualDocument())
                    .map(ossMap::get)
                    .map(SysOssVo::getOriginalName)
                    .orElse("-")
            );
            long currentFolder = document.getFolderId();
            if (currentFolder != 0L) {
                for (;;) {
                    Folder folder = folderMap.get(currentFolder);
                    if (folder.getType().equals(FolderTypeConst.CENTER)) {
                        excel.setCenterId(currentFolder);
                        excel.setCenterName(folder.getName());
                        break;
                    }
                    if (folder.getType().equals(FolderTypeConst.NORMAL)) {
                        if (folder.getParentId() != null) {
                            currentFolder = folder.getParentId();
                            continue;
                        } else {
                            break;
                        }
                    }
                    break;
                }
            } else {
                excel.setCenterName("-");
            }
            excel.setPlanSubmitter(
                Optional.ofNullable(document.getPlanSubmitter())
                    .map(userMap::get)
                    .map(e -> e.getNickName() + "(" + e.getEmail() + ")")
                    .orElse("-")
            );
            excel.setDeadline(
                Optional.ofNullable(document.getSubmitDeadline())
                    .map(Date::toString)
                    .orElse("-")
            );
            Date submitTime = Optional.ofNullable(document.getSubmitTime()).orElse(new Date());
            excel.setDaysOverdue(
                Optional.ofNullable(document.getSubmitDeadline())
                    .map(deadline -> {
                        LocalDate dueLocalDate = deadline.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
                        LocalDate submitLocalDate = submitTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
                        if (!submitLocalDate.isAfter(dueLocalDate)) {
                            return 0L;
                        }
                        return ChronoUnit.DAYS.between(dueLocalDate, submitLocalDate);
                    })
                    .orElse(0L)
            );
            excel.setCreateBy(
                Optional.ofNullable(document.getCreateBy())
                    .map(userMap::get)
                    .map(SysUser::getNickName)
                    .orElse("-")
            );
            StringBuilder path = new StringBuilder();
            if (document.getFolderId() != 0L) {
                Folder current = folderMap.get(document.getFolderId());
                for (;;) {
                    path.insert(0, current.getName()).insert(0, "/");
                    if (current.getParentId() == null) {
                        break;
                    }
                    current = folderMap.get(current.getParentId());
                }
                excel.setPath(path.toString());
            } else {
                excel.setName("-");
            }
            excel.setSubmitTime(
                Optional.ofNullable(document.getSubmitTime())
                    .map(Date::toString)
                    .orElse("-")
            );
            excel.setSubmitter(
                Optional.ofNullable(document.getSubmitter())
                    .map(userMap::get)
                    .map(SysUser::getNickName)
                    .orElse("-")
            );
            excel.setProjectName(project.getName());
            if (StringUtils.isNotBlank(document.getPlanType())) {
                Dict label = JsonUtils.parseMap(planType.get(document.getPlanType()));
                excel.setPlanType(LocaleContextHolder.getLocale().getLanguage().equals("zh") ? label.getStr("zh_CN") : label.getStr("en_US"));
            }
            list.add(excel);
        }
        return list;
    }

    @Override
    public List<Document> listOnWorkbench() {
        return baseMapper.selectList(buildListWrapperOnWorkbench());
    }

    private LambdaQueryWrapper<Document> buildListWrapperOnWorkbench() {
        LambdaQueryWrapper<Document> wrapper = Wrappers.lambdaQuery(Document.class);
        wrapper.and(w -> w
            .eq(Document::getPlanSubmitter, LoginHelper.getUserId())
            .eq(Document::getStatus, DocumentStatusEnum.UN_UPLOAD.getValue())
        );
        wrapper.or(w -> w
            .eq(Document::getCreateBy, LoginHelper.getUserId())
            .in(Document::getStatus, List.of(DocumentStatusEnum.UN_AUDIT.getValue(), DocumentStatusEnum.UN_ARCHIEVED.getValue()))
        );
        return wrapper;
    }

    private LambdaQueryWrapper<Document> buildSpecifyListWrapper(DocumentListOnSpecifyBo bo, List<Long> folderIds) {
        return Wrappers.lambdaQuery(Document.class)
            .eq(Document::getCreateBy, LoginHelper.getUserId())
            .in(Document::getFolderId, folderIds.isEmpty() ? List.of(-1L) : folderIds)
            .like(StringUtils.isNotBlank(bo.getName()), Document::getName, bo.getName())
            .in(Document::getStatus, List.of(DocumentStatusEnum.UN_UPLOAD.getValue(), DocumentStatusEnum.AUDIT_REJECT.getValue()))
            .orderByDesc(Document::getId);
    }

    private void buildFolders(List<Long> selected, Folder folder, Map<Long, List<Folder>> childrenMap) {
        selected.add(folder.getId());
        List<Folder> childrens = childrenMap.get(folder.getId());
        if (childrens == null || childrens.isEmpty()) {
            return;
        }
        childrens.forEach(e -> buildFolders(selected, e, childrenMap));
    }

    private LambdaQueryWrapper<Document> buildFilingListWrapper(TaskCenterFilingListBo bo, List<Long> folderIds) {
        return Wrappers.lambdaQuery(Document.class)
            .like(StringUtils.isNotBlank(bo.getName()), Document::getName, bo.getName())
            .eq(Document::getCreateBy, LoginHelper.getUserId())
            .in(!folderIds.isEmpty(), Document::getFolderId, folderIds)
            .eq(Document::getStatus, DocumentStatusEnum.UN_ARCHIEVED.getValue())
            .orderByDesc(Document::getId);
    }

    private LambdaQueryWrapper<Document> buildOnSearchWrapper(DocumentSearchBo bo, List<Long> projectIds) {
        return Wrappers.lambdaQuery(Document.class)
            .in(StringUtils.isNotBlank(bo.getProjectName()) || StringUtils.isNotBlank(bo.getProjectCode()), Document::getProjectId, projectIds.isEmpty() ? List.of(-1L) : projectIds)
            .like(StringUtils.isNotBlank(bo.getName()), Document::getName, bo.getName())
            .eq(bo.getStatus() != null, Document::getStatus, bo.getStatus())
            .eq(bo.getType() != null, Document::getPlanType, bo.getType())
            .ge(bo.getParams().containsKey("createTimeEarliest"), Document::getCreateTime, bo.getParams().get("createTimeEarliest"))
            .le(bo.getParams().containsKey("createTimeLatest"), Document::getCreateTime, bo.getParams().get("createTimeLatest"))
            .ge(bo.getParams().containsKey("updateTimeEarliest"), Document::getUpdateTime, bo.getParams().get("updateTimeEarliest"))
            .le(bo.getParams().containsKey("updateTimeLatest"), Document::getUpdateTime, bo.getParams().get("updateTimeLatest"))
            .orderByDesc(Document::getId);
    }

    private LambdaQueryWrapper<Document> buildAuditListWrapper(TaskCenterAuditListBo bo, List<Long> folderIds) {
        return Wrappers.lambdaQuery(Document.class)
            .like(StringUtils.isNotBlank(bo.getName()), Document::getName, bo.getName())
            .eq(Document::getCreateBy, LoginHelper.getUserId())
            .in(!folderIds.isEmpty(), Document::getFolderId, folderIds)
            .eq(Document::getStatus, DocumentStatusEnum.UN_AUDIT.getValue())
            .orderByDesc(Document::getId);
    }

    private LambdaQueryWrapper<Document> buildSubmissionListWrapper(TaskCenterSubmissionListBo bo, List<Long> folderIds) {
        return Wrappers.lambdaQuery(Document.class)
            .like(StringUtils.isNotBlank(bo.getName()), Document::getName, bo.getName())
            .eq(bo.getStatus() != null, Document::getStatus, bo.getStatus())
            .eq(Document::getPlanSubmitter, LoginHelper.getUserId())
            .in(!folderIds.isEmpty(), Document::getFolderId, folderIds)
            .in(Document::getStatus, List.of(DocumentStatusEnum.UN_UPLOAD.getValue(), DocumentStatusEnum.AUDIT_REJECT.getValue()))
            .orderByDesc(Document::getId);
    }

    private DocumentAuditLog buildLog(Document document, String type, DocumentAuditBo bo) {
        DocumentAuditLog log = new DocumentAuditLog();
        log.setDocumentId(document.getId());
        log.setUploadVersion(document.getActualDocument());
        log.setAuditorType(type);
        log.setAuditor(LoginHelper.getUserId());
        log.setResult(bo.getResult());
        log.setRejectReason(bo.getRejectReason());
        log.setAuditTime(new Date());
        log.setTenantId(TenantHelper.getTenantId());
        log.setSubmitter(document.getSubmitter());
        return log;
    }
}
