# DocumentApplicationService.java

    package com.yumu.noveltranslator.application.service;

    import com.yumu.noveltranslator.port.dto.entity.DocumentInfoResponse;
    import com.yumu.noveltranslator.port.dto.translation.DocumentTranslationRequest;
    import com.yumu.noveltranslator.port.dto.translation.DocumentTranslationResponse;
    import com.yumu.noveltranslator.domain.model.Document;
    import com.yumu.noveltranslator.domain.model.TranslationTask;
    import com.yumu.noveltranslator.enums.TranslationStatus;
    import com.yumu.noveltranslator.port.in.CollabPort;
    import com.yumu.noveltranslator.port.out.DocumentRepositoryPort;
    import com.yumu.noveltranslator.port.out.TranslationRepositoryPort;
    import com.yumu.noveltranslator.domain.service.TranslationStateMachine;
    import lombok.RequiredArgsConstructor;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;

    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.List;
    import java.util.UUID;
    import java.util.stream.Collectors;

    /**
     * 文档管理应用服务
     */
    @Service
    @RequiredArgsConstructor
    public class DocumentApplicationService implements com.yumu.noveltranslator.port.in.DocumentPort {

        @Value("${translation.upload-dir:#{systemProperties['user.home']}/novel-translator/uploads}")
        private String uploadDir;

        private final DocumentRepositoryPort documentPort;
        private final TranslationRepositoryPort translationPort;
        private final TranslationStateMachine stateMachine;
        private final CollabPort collabPort;
        private final com.yumu.noveltranslator.port.in.TranslationTaskPort translationTaskPort;

        /**
         * 上传文档
         */
        public Document uploadDocument(Long userId, MultipartFile file, DocumentTranslationRequest request) throws IOException {
            // 创建上传目录
            Path uploadPath = Paths.get(uploadDir);
            if (!Files.exists(uploadPath)) {
                Files.createDirectories(uploadPath);
            }

            // 生成唯一文件名
            String originalFilename = file.getOriginalFilename();
            String extension = originalFilename != null && originalFilename.contains(".")
                    ? originalFilename.substring(originalFilename.lastIndexOf("."))
                    : "";
            String filename = UUID.randomUUID().toString() + extension;
            Path filePath = uploadPath.resolve(filename);

            // 保存文件
            Files.copy(file.getInputStream(), filePath);

            // 创建文档记录
            Document doc = new Document();
            doc.setUserId(userId);
            doc.setName(originalFilename);
            doc.setPath(filePath.toString());
            doc.setSourceLang(request.getSourceLang());
            doc.setTargetLang(request.getTargetLang());
            doc.setFileType(extension.replace(".", "").toLowerCase());
            doc.setFileSize(file.getSize());
            doc.setStatus(TranslationStatus.PENDING.getValue());
            doc.setMode(request.getMode());
            doc.setCreateTime(LocalDateTime.now());

            documentPort.save(doc);

            return doc;
        }

        /**
         * 获取用户文档列表
         */
        public List<Document> getUserDocuments(Long userId, String status) {
            List<Document> documents = documentPort.findByUserId(userId);
            if (status != null && !"all".equals(status)) {
                return documents.stream()
                        .filter(doc -> status.equals(doc.getStatus()))
                        .collect(Collectors.toList());
            }
            return documents;
        }

        /**
         * 获取文档详情
         */
        public Document getDocumentById(Long docId, Long userId) {
            return documentPort.findByIdAndUserId(docId, userId).orElse(null);
        }

        /**
         * 删除文档
         */
        public boolean deleteDocument(Long docId, Long userId) {
            return documentPort.findByIdAndUserId(docId, userId).map(doc -> {
                try {
                    Files.deleteIfExists(Paths.get(doc.getPath()));
                } catch (IOException e) {
                    // 忽略文件删除失败
                }
                documentPort.markDeleted(docId);
                return true;
            }).orElse(false);
        }

        /**
         * 重新翻译文档（仅重置状态，实际翻译由前端 SSE 触发）
         */
        public boolean retryTranslation(Long docId, Long userId) {
            return documentPort.findByIdAndUserId(docId, userId).map(doc -> {
                doc.setStatus(TranslationStatus.PENDING.getValue());
                doc.setErrorMessage(null);
                doc.setUpdateTime(LocalDateTime.now());
                documentPort.update(doc);
                // 同时重置关联的所有翻译任务状态
                List<TranslationTask> tasks = translationPort.findTasksByDocumentId(docId);
                for (TranslationTask task : tasks) {
                    task.setStatus("pending");
                    task.setErrorMessage(null);
                    task.setProgress(0);
                    task.setUpdateTime(LocalDateTime.now());
                    translationPort.updateTask(task);
                }
                return true;
            }).orElse(false);
        }

        /**
         * 转换 Document 为 DocumentInfoResponse
         */
        public DocumentInfoResponse toDocumentInfoResponse(Document doc) {
            if (doc == null) {
                return null;
            }
            DocumentInfoResponse response = new DocumentInfoResponse();
            response.setId(doc.getId());
            response.setName(doc.getName());
            response.setFileType(doc.getFileType());
            response.setFileSize(doc.getFileSize());
            response.setSourceLang(doc.getSourceLang());
            response.setTargetLang(doc.getTargetLang());
            response.setTaskId(doc.getTaskId());
            response.setStatus(doc.getStatus());
            response.setProgress(resolveProgress(doc));
            response.setCreateTime(doc.getCreateTime() != null
                    ? doc.getCreateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
                    : null);
            response.setCompletedTime(doc.getCompletedTime() != null
                    ? doc.getCompletedTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
                    : null);
            response.setErrorMessage(doc.getErrorMessage());
            return response;
        }

        /**
         * 上传文档并根据翻译模式自动启动翻译
         * 团队模式：添加章节到协作项目或创建新项目；fast/expert 模式：创建翻译任务
         */
        public DocumentTranslationResponse uploadAndStartTranslation(
                Long userId, MultipartFile file, DocumentTranslationRequest request) throws IOException {

            Document doc = uploadDocument(userId, file, request);

            if ("team".equals(request.getMode())) {
                // 团队模式：文档已通过 documentId 关联（uploadDocument 创建了 doc，但没传 projectId）
                // 实际上团队模式需要 projectId，所以这个方法由调用方决定走哪条路
                // 为保持接口简洁，团队模式不使用此方法，调用方直接用 uploadDocument + collabPort
                throw new UnsupportedOperationException("团队模式请使用 uploadDocument 后手动调用 CollabPort");
            }

            // fast/expert 模式：创建任务后直接异步启动翻译
            TranslationTask task = translationTaskPort.createDocumentTask(userId, doc);
            translationTaskPort.startDocumentTranslation(task, doc);

            DocumentTranslationResponse response = new DocumentTranslationResponse();
            response.setTaskId(task.getTaskId());
            response.setDocumentId(doc.getId());
            response.setDocumentName(doc.getName());
            response.setStatus(task.getStatus());
            response.setProjectId(null);
            response.setMessage("文档上传成功");

            return response;
        }

        /**
         * 取消翻译：先尝试取消翻译任务，无任务时直接更新文档状态
         */
        public boolean cancelTranslation(Long docId, Long userId) {
            TranslationTask task = translationTaskPort.getTaskByDocumentId(docId);
            if (task != null) {
                if (!task.getUserId().equals(userId)) {
                    return false;
                }
                return translationTaskPort.cancelTask(task.getTaskId(), userId);
            }

            // 翻译任务不存在，直接更新文档状态
            Document doc = documentPort.findByIdAndUserId(docId, userId).orElse(null);
            if (doc == null) {
                return false;
            }
            if (!TranslationStatus.PENDING.getValue().equals(doc.getStatus())
                    && !TranslationStatus.PROCESSING.getValue().equals(doc.getStatus())) {
                return false;
            }
            doc.setStatus(TranslationStatus.FAILED.getValue());
            doc.setErrorMessage("用户取消翻译");
            doc.setUpdateTime(java.time.LocalDateTime.now());
            documentPort.update(doc);
            return true;
        }

        /**
         * 从 Document 或关联的 TranslationTask 获取真实进度
         */
        private int resolveProgress(Document doc) {
            if (TranslationStatus.COMPLETED.getValue().equals(doc.getStatus())) {
                return 100;
            }
            if (TranslationStatus.PROCESSING.getValue().equals(doc.getStatus()) || TranslationStatus.PENDING.getValue().equals(doc.getStatus())) {
                int realProgress = getRealProgress(doc);
                if (realProgress > 0) {
                    return realProgress;
                }
                // 返回基于时间的平滑进度，提升用户体验
                if (doc.getCreateTime() != null) {
                    long elapsedSeconds = java.time.Duration.between(doc.getCreateTime(), LocalDateTime.now()).getSeconds();
                    // 预估 3 分钟完成，每分钟约 33%，最多到 95%
                    int estimated = Math.min(95, (int) (elapsedSeconds * 100.0 / 180.0));
                    return Math.max(5, estimated); // 至少 5%
                }
            }
            return 0;
        }

        private int getRealProgress(Document doc) {
            if (doc.getTaskId() != null) {
                return translationPort.findTaskByTaskId(doc.getTaskId())
                        .map(task -> task.getProgress() != null ? task.getProgress() : 0)
                        .orElse(0);
            }
            return 0;
        }
    }