CollabProjectApplicationService.java

package com.yumu.noveltranslator.application.service;

import com.yumu.noveltranslator.config.tenant.TenantContext;
import com.yumu.noveltranslator.port.dto.collab.CollabProjectResponse;
import com.yumu.noveltranslator.port.dto.collab.CreateCollabProjectRequest;
import com.yumu.noveltranslator.port.dto.collab.InviteMemberRequest;
import com.yumu.noveltranslator.port.dto.collab.ProjectMemberResponse;
import com.yumu.noveltranslator.port.dto.common.PageResponse;
import com.yumu.noveltranslator.port.in.CollabPort;
import com.yumu.noveltranslator.domain.model.CollabChapterTask;
import com.yumu.noveltranslator.domain.model.CollabInviteCode;
import com.yumu.noveltranslator.domain.model.CollabProject;
import com.yumu.noveltranslator.domain.model.CollabProjectMember;
import com.yumu.noveltranslator.domain.model.Document;
import com.yumu.noveltranslator.domain.model.User;
import com.yumu.noveltranslator.enums.ChapterTaskStatus;
import com.yumu.noveltranslator.enums.CollabProjectStatus;
import com.yumu.noveltranslator.enums.ErrorCodeEnum;
import com.yumu.noveltranslator.enums.ProjectMemberRole;
import com.yumu.noveltranslator.enums.TranslationStatus;
import com.yumu.noveltranslator.domain.event.ChapterSplitEvent;
import com.yumu.noveltranslator.port.out.CollaborationRepositoryPort;
import com.yumu.noveltranslator.port.out.DocumentRepositoryPort;
import com.yumu.noveltranslator.port.out.UserRepositoryPort;
import com.yumu.noveltranslator.exception.BusinessException;
import com.yumu.noveltranslator.domain.service.MultiAgentTranslationService;
import com.yumu.noveltranslator.domain.service.CollabStateMachine;
import com.yumu.noveltranslator.domain.service.CollabEventPublisher;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * 协作项目管理服务
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class CollabProjectApplicationService implements com.yumu.noveltranslator.port.in.CollabPort {

    private final CollaborationRepositoryPort collabPort;
    private final DocumentRepositoryPort documentPort;
    private final UserRepositoryPort userPort;
    private final CollabStateMachine collabStateMachine;
    private final MultiAgentTranslationService multiAgentTranslationService;
    private final ApplicationEventPublisher eventPublisher;

    /**
     * 创建协作项目,创建者自动成为 OWNER
     */
    @Transactional
    public CollabProjectResponse createProject(CreateCollabProjectRequest request, Long userId) {
        CollabProject project = new CollabProject();
        project.setName(request.getName());
        project.setDescription(request.getDescription());
        project.setOwnerId(userId);
        project.setSourceLang(request.getSourceLang());
        project.setTargetLang(request.getTargetLang());
        project.setStatus(CollabProjectStatus.DRAFT.getValue());
        project.setProgress(0);
        collabPort.saveProject(project);

        // 创建者自动成为 OWNER
        CollabProjectMember owner = new CollabProjectMember();
        owner.setProjectId(project.getId());
        owner.setUserId(userId);
        owner.setRole(ProjectMemberRole.OWNER.getValue());
        owner.setInviteStatus("ACTIVE");
        owner.setJoinedTime(LocalDateTime.now());
        collabPort.saveMember(owner);

        log.info("创建协作项目: projectId={}, name={}, ownerId={}", project.getId(), project.getName(), userId);
        return toProjectResponse(project);
    }

    /**
     * 从上传文档创建协作项目(团队模式)
     */
    @Transactional
    public CollabPort.TeamProjectCreateResult createProjectFromDocument(Long userId, Long documentId, String documentName,
                                                              String filePath, String fileType,
                                                              String sourceLang, String targetLang) {
        String content;
        try {
            content = Files.readString(Paths.get(filePath), java.nio.charset.StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "读取文档失败: " + e.getMessage());
        }

        if (content == null || content.trim().isEmpty()) {
            throw new BusinessException(ErrorCodeEnum.PARAMETER_ERROR, "文档内容为空");
        }

        List<String> chapters = splitIntoChapters(content);

        return doCreateProject(userId, documentId, documentName, chapters, sourceLang, targetLang);
    }

    private CollabPort.TeamProjectCreateResult doCreateProject(Long userId, Long documentId, String documentName,
                                                     List<String> chapters, String sourceLang, String targetLang) {
        CollabProject project = createProjectAndOwner(userId, documentId, documentName, sourceLang, targetLang);

        log.info("团队模式创建项目(窄事务): projectId={}, docName={}, chapters={}, 将异步插入",
                project.getId(), documentName, chapters.size());

        // 更新文档状态为处理中
        documentPort.findById(documentId).ifPresent(doc -> {
            doc.setStatus(TranslationStatus.PROCESSING.getValue());
            doc.setUpdateTime(LocalDateTime.now());
            documentPort.update(doc);
        });

        eventPublisher.publishEvent(new ChapterSplitEvent(
                project.getId(), userId, documentId, documentName,
                chapters, sourceLang, targetLang));

        return new CollabPort.TeamProjectCreateResult(project.getId(), documentName, chapters.size());
    }

    /**
     * 窄事务:仅创建项目和 OWNER 成员
     */
    @Transactional
    protected CollabProject createProjectAndOwner(Long userId, Long documentId, String documentName,
                                                    String sourceLang, String targetLang) {
        CollabProject project = new CollabProject();
        project.setName(documentName);
        project.setDescription("团队模式自动创建");
        project.setOwnerId(userId);
        project.setDocumentId(documentId);
        project.setSourceLang(sourceLang != null ? sourceLang : "auto");
        project.setTargetLang(targetLang);
        project.setStatus(CollabProjectStatus.DRAFT.getValue());
        project.setProgress(0);
        collabPort.saveProject(project);

        CollabProjectMember owner = new CollabProjectMember();
        owner.setProjectId(project.getId());
        owner.setUserId(userId);
        owner.setRole(ProjectMemberRole.OWNER.getValue());
        owner.setInviteStatus("ACTIVE");
        owner.setJoinedTime(LocalDateTime.now());
        collabPort.saveMember(owner);

        return project;
    }

    /**
     * 将文档章节添加到已有协作项目
     */
    @Transactional
    public int addChaptersToProject(Long userId, Long projectId, Document document) {
        CollabProject project = collabPort.findProjectById(projectId).orElse(null);
        if (project == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "项目不存在: " + projectId);
        }

        // 权限校验
        var member = collabPort.findMemberByProjectAndUser(projectId, userId);
        if (member == null && !project.getOwnerId().equals(userId)) {
            throw new BusinessException(ErrorCodeEnum.FORBIDDEN, "无权向该项目添加章节");
        }

        String content;
        try {
            content = Files.readString(Paths.get(document.getPath()), java.nio.charset.StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "读取文档失败: " + e.getMessage());
        }

        if (content == null || content.trim().isEmpty()) {
            throw new BusinessException(ErrorCodeEnum.PARAMETER_ERROR, "文档内容为空");
        }

        List<String> chapters = splitIntoChapters(content);

        return doAddChaptersToProject(project, document, chapters);
    }

    @Transactional
    private int doAddChaptersToProject(CollabProject project, Document document, List<String> chapters) {
        Long projectId = project.getId();

        if (project.getDocumentId() == null) {
            project.setDocumentId(document.getId());
            collabPort.updateProject(project);
        }

        List<CollabChapterTask> existingChapters = collabPort.findChapterTasksByProjectId(projectId);
        int nextChapterNumber = existingChapters.stream()
                .mapToInt(CollabChapterTask::getChapterNumber)
                .max()
                .orElse(0) + 1;

        for (String chapterText : chapters) {
            CollabChapterTask chapter = new CollabChapterTask();
            chapter.setProjectId(projectId);
            chapter.setChapterNumber(nextChapterNumber++);
            chapter.setTitle("第 " + chapter.getChapterNumber() + " 章");
            chapter.setSourceText(chapterText);
            chapter.setTargetText(null);
            chapter.setStatus(ChapterTaskStatus.UNASSIGNED.getValue());
            chapter.setProgress(0);
            chapter.setSourceWordCount(chapterText.length());
            collabPort.saveChapterTask(chapter);
        }

        document.setStatus(TranslationStatus.PROCESSING.getValue());
        document.setUpdateTime(java.time.LocalDateTime.now());
        documentPort.update(document);

        log.info("添加章节到项目: projectId={}, docName={}, chapters={}", projectId, document.getName(), chapters.size());
        return chapters.size();
    }

    /**
     * 启动多 Agent 翻译
     */
    public void startMultiAgentTranslation(Long projectId) {
        multiAgentTranslationService.startMultiAgentTranslation(projectId);
    }

    /**
     * 智能章节分割
     */
    private List<String> splitIntoChapters(String content) {
        String[] lines = content.split("\n");
        List<String> chapters = new ArrayList<>();
        StringBuilder currentChapter = new StringBuilder();
        boolean hasChapterTitle = false;

        java.util.regex.Pattern chapterPattern = java.util.regex.Pattern.compile(
                "^\\s*(?:\\*\\*?)?\\s*(?:" +
                        "(?:第\\s*[零一二三四五六七八九十百千\\d]+\\s*(?:章|节|回|卷|篇))" +
                        "|(?:chapter\\s+[ivxlcdm\\d]+\\s*[::]?\\s*.*)" +
                        "|(?:ch\\.?\\s*\\d+)" +
                        "|(?:part\\s+[ivxlcdm\\d]+\\s*[::]?\\s*.*)" +
                        "|(?:(?:[一二三四五六七八九十]+)、)" +
                        ")",
                java.util.regex.Pattern.CASE_INSENSITIVE
        );

        for (String line : lines) {
            String trimmed = line.trim();

            if (chapterPattern.matcher(trimmed).find()) {
                if (currentChapter.length() > 0) {
                    String chapterText = currentChapter.toString().trim();
                    if (!chapterText.isEmpty()) {
                        chapters.add(chapterText);
                    }
                    currentChapter = new StringBuilder();
                }
                hasChapterTitle = true;
                currentChapter.append(trimmed).append("\n");
            } else {
                currentChapter.append(line).append("\n");
            }
        }

        if (currentChapter.length() > 0) {
            String chapterText = currentChapter.toString().trim();
            if (!chapterText.isEmpty()) {
                chapters.add(chapterText);
            }
        }

        if (!hasChapterTitle) {
            chapters.clear();
            String[] paragraphs = content.split("\n+");
            StringBuilder current = new StringBuilder();
            int maxCharsPerChapter = 2000;

            for (String p : paragraphs) {
                String pTrimmed = p.trim();
                if (pTrimmed.isEmpty()) continue;
                if (current.length() + pTrimmed.length() > maxCharsPerChapter && current.length() > 0) {
                    chapters.add(current.toString().trim());
                    current = new StringBuilder();
                }
                current.append(pTrimmed).append("\n");
            }
            if (current.length() > 0) {
                chapters.add(current.toString().trim());
            }
        }

        if (chapters.isEmpty()) {
            chapters.add(content.trim());
        }

        log.debug("章节分割: 检测到章节标题={}, 分割出 {} 章", hasChapterTitle, chapters.size());
        return chapters;
    }

    /**
     * 获取项目详情
     */
    public CollabProjectResponse getProjectById(Long projectId) {
        CollabProject project = collabPort.findProjectById(projectId).orElse(null);
        if (project == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "项目不存在: " + projectId);
        }
        return toProjectResponse(project);
    }

    /**
     * 获取用户参与的项目列表
     */
    public PageResponse<CollabProjectResponse> listByUserId(Long userId, int page, int pageSize) {
        try {
            TenantContext.setBypassTenant(true);
            List<CollabProject> allProjects = collabPort.findProjectsByMemberUserId(userId);
            long total = allProjects.size();
            int fromIndex = Math.min((page - 1) * pageSize, (int) total);
            int toIndex = Math.min(fromIndex + pageSize, (int) total);
            List<CollabProject> pagedProjects = fromIndex < total
                    ? allProjects.subList(fromIndex, toIndex)
                    : List.of();
            Map<Long, User> userMap = batchLoadOwnerUsers(pagedProjects);
            List<CollabProjectResponse> responseList = pagedProjects.stream()
                    .map(p -> toProjectResponse(p, userMap))
                    .collect(Collectors.toList());
            return PageResponse.of(page, pageSize, total, responseList);
        } finally {
            TenantContext.setBypassTenant(false);
        }
    }

    /**
     * 更新项目信息
     */
    @Transactional
    public CollabProjectResponse updateProject(Long projectId, CreateCollabProjectRequest request, Long userId) {
        CollabProject project = collabPort.findProjectById(projectId).orElse(null);
        if (project == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "项目不存在: " + projectId);
        }

        project.setName(request.getName());
        project.setDescription(request.getDescription());
        project.setSourceLang(request.getSourceLang());
        project.setTargetLang(request.getTargetLang());
        collabPort.updateProject(project);

        return toProjectResponse(project);
    }

    /**
     * 变更项目状态
     */
    @Transactional
    public void changeProjectStatus(Long projectId, CollabProjectStatus targetStatus, Long userId) {
        CollabProject project = collabPort.findProjectById(projectId).orElse(null);
        if (project == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "项目不存在: " + projectId);
        }

        CollabProjectStatus current = CollabProjectStatus.fromValue(project.getStatus());
        collabStateMachine.transitionProject(project, targetStatus);
        if (targetStatus == CollabProjectStatus.COMPLETED) {
            project.setProgress(100);
        }
        collabPort.updateProject(project);

        log.info("项目状态变更: projectId={}, {} -> {}", projectId, current, targetStatus);
    }

    /**
     * 生成项目邀请码
     */
    @Transactional
    public CollabPort.InviteCodeResult generateInviteCode(Long projectId, Long operatorId) {
        CollabProject project = collabPort.findProjectById(projectId).orElse(null);
        if (project == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "项目不存在: " + projectId);
        }

        String code = generateRandomInviteCode();
        LocalDateTime expiresAt = LocalDateTime.now().plusHours(72);

        CollabInviteCode inviteCode = new CollabInviteCode();
        inviteCode.setProjectId(projectId);
        inviteCode.setCode(code);
        inviteCode.setExpiresAt(expiresAt);
        inviteCode.setUsed(0);
        collabPort.saveInviteCode(inviteCode);

        log.info("生成邀请码: projectId={}, code={}, expiresAt={}", projectId, code, expiresAt);
        return new CollabPort.InviteCodeResult(code, expiresAt);
    }

    private String generateRandomInviteCode() {
        String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
        java.security.SecureRandom random = new java.security.SecureRandom();
        StringBuilder code = new StringBuilder(8);
        for (int i = 0; i < 8; i++) {
            code.append(chars.charAt(random.nextInt(chars.length())));
        }
        if (collabPort.findInviteCodeByCode(code.toString()) != null) {
            return generateRandomInviteCode();
        }
        return code.toString();
    }

    /**
     * 邀请成员
     */
    @Transactional
    public ProjectMemberResponse inviteMember(Long projectId, InviteMemberRequest request, Long inviterId) {
        User user = userPort.findByEmail(request.getEmail()).orElse(null);
        if (user == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "用户不存在: " + request.getEmail());
        }

        var existing = collabPort.findMemberByProjectAndUser(projectId, user.getId());
        if (existing != null) {
            throw new BusinessException(ErrorCodeEnum.INVALID_STATE, "该用户已是项目成员");
        }

        String inviteCode = UUID.randomUUID().toString().replace("-", "").substring(0, 16);

        CollabProjectMember member = new CollabProjectMember();
        member.setProjectId(projectId);
        member.setUserId(user.getId());
        member.setRole(request.getRole().getValue());
        member.setInviteCode(inviteCode);
        member.setInviteStatus("INVITED");
        collabPort.saveMember(member);

        log.info("邀请成员: projectId={}, email={}, role={}", projectId, request.getEmail(), request.getRole());
        return toMemberResponse(member, user);
    }

    /**
     * 通过邀请码加入项目
     */
    @Transactional
    public ProjectMemberResponse joinByInviteCode(String inviteCode, Long userId) {
        CollabInviteCode codeRecord = collabPort.findValidInviteCode(inviteCode);
        if (codeRecord == null) {
            CollabInviteCode anyCode = collabPort.findInviteCodeByCode(inviteCode);
            if (anyCode == null) {
                throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "邀请码无效");
            }
            if (anyCode.getUsed() == 1) {
                throw new BusinessException(ErrorCodeEnum.INVALID_STATE, "邀请码已被使用");
            }
            if (anyCode.getExpiresAt().isBefore(LocalDateTime.now())) {
                throw new BusinessException(ErrorCodeEnum.INVALID_STATE, "邀请码已过期");
            }
            throw new BusinessException(ErrorCodeEnum.INVALID_STATE, "邀请码不可用");
        }

        collabPort.markInviteCodeAsUsed(codeRecord.getId());

        try {
            TenantContext.setBypassTenant(true);

            CollabProject project = collabPort.findProjectById(codeRecord.getProjectId()).orElse(null);
            if (project == null) {
                throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "关联项目不存在");
            }
            if (!CollabProjectStatus.ACTIVE.getValue().equals(project.getStatus())) {
                throw new BusinessException(ErrorCodeEnum.INVALID_STATE, "项目当前不可加入");
            }

            var existing = collabPort.findMemberByProjectAndUser(codeRecord.getProjectId(), userId);
            if (existing != null) {
                throw new BusinessException(ErrorCodeEnum.INVALID_STATE, "您已是该项目成员");
            }

            CollabProjectMember member = new CollabProjectMember();
            member.setProjectId(codeRecord.getProjectId());
            member.setUserId(userId);
            member.setRole(ProjectMemberRole.TRANSLATOR.getValue());
            member.setInviteCode(inviteCode);
            member.setInviteStatus("ACTIVE");
            member.setJoinedTime(LocalDateTime.now());
            member.setTenantId(project.getTenantId());
            collabPort.saveMember(member);

            User user = userPort.findById(userId).orElse(null);
            log.info("加入项目: userId={}, projectId={}, inviteCode={}", userId, codeRecord.getProjectId(), inviteCode);
            return toMemberResponse(member, user);
        } finally {
            TenantContext.setBypassTenant(false);
        }
    }

    /**
     * 获取项目成员列表(分页)
     */
    public PageResponse<ProjectMemberResponse> getMembers(Long projectId, int page, int pageSize) {
        List<CollabProjectMember> allMembers;
        try {
            TenantContext.setBypassTenant(true);
            allMembers = collabPort.findMembersByProjectId(projectId);
        } finally {
            TenantContext.setBypassTenant(false);
        }
        long total = allMembers.size();
        int fromIndex = Math.min((page - 1) * pageSize, (int) total);
        int toIndex = Math.min(fromIndex + pageSize, (int) total);
        List<CollabProjectMember> pagedMembers = fromIndex < total
                ? allMembers.subList(fromIndex, toIndex)
                : List.of();

        Set<Long> userIds = new HashSet<>();
        for (CollabProjectMember m : pagedMembers) {
            if (m.getUserId() != null) userIds.add(m.getUserId());
        }
        Map<Long, User> userMap = new HashMap<>();
        for (Long uid : userIds) {
            userPort.findById(uid).ifPresent(u -> userMap.put(uid, u));
        }

        List<ProjectMemberResponse> responseList = pagedMembers.stream()
                .map(m -> toMemberResponse(m, userMap.get(m.getUserId())))
                .collect(Collectors.toList());
        return PageResponse.of(page, pageSize, total, responseList);
    }

    /**
     * 移除成员
     */
    @Transactional
    public void removeMember(Long projectId, Long memberId, Long operatorId) {
        var member = collabPort.findMemberById(memberId).orElse(null);
        if (member == null || !member.getProjectId().equals(projectId)) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "成员不存在");
        }

        if (ProjectMemberRole.OWNER.getValue().equals(member.getRole())) {
            throw new BusinessException(ErrorCodeEnum.INVALID_STATE, "所有者不能直接移除,需先转让项目");
        }

        member.setInviteStatus("REMOVED");
        collabPort.updateMember(member);
    }

    /**
     * 删除项目
     */
    @Transactional
    public void deleteProject(Long projectId, Long userId) {
        CollabProject project = collabPort.findProjectById(projectId).orElse(null);
        if (project == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "项目不存在");
        }
        if (!project.getOwnerId().equals(userId)) {
            throw new BusinessException(ErrorCodeEnum.FORBIDDEN, "只有项目所有者可以删除项目");
        }

        // 级联逻辑删除:评论 → 章节 → 成员 → 项目
        List<CollabChapterTask> chapters = collabPort.findChapterTasksByProjectId(projectId);
        for (CollabChapterTask chapter : chapters) {
            collabPort.deleteCommentsByChapterTaskId(chapter.getId());
        }
        collabPort.deleteChapterTasksByProjectId(projectId);
        collabPort.deleteMembersByProjectId(projectId);
        collabPort.deleteProject(projectId);

        log.info("删除协作项目: projectId={}, name={}, ownerId={}", projectId, project.getName(), userId);
    }

    private CollabProjectResponse toProjectResponse(CollabProject project) {
        return toProjectResponse(project, Map.of());
    }

    private CollabProjectResponse toProjectResponse(CollabProject project, Map<Long, User> userMap) {
        CollabProjectResponse resp = new CollabProjectResponse();
        resp.setId(project.getId());
        resp.setName(project.getName());
        resp.setDescription(project.getDescription());
        resp.setOwnerId(project.getOwnerId());
        resp.setSourceLang(project.getSourceLang());
        resp.setTargetLang(project.getTargetLang());
        resp.setStatus(project.getStatus());
        resp.setProgress(project.getProgress());
        resp.setCreateTime(project.getCreateTime());
        resp.setUpdateTime(project.getUpdateTime());

        User owner = userMap.get(project.getOwnerId());
        if (owner == null) {
            owner = userPort.findById(project.getOwnerId()).orElse(null);
        }
        if (owner != null) {
            resp.setOwnerName(owner.getUsername());
        }

        int memberCount = collabPort.countMembersByProjectId(project.getId());
        resp.setMemberCount(memberCount);

        return resp;
    }

    private Map<Long, User> batchLoadOwnerUsers(List<CollabProject> projects) {
        Set<Long> userIds = new HashSet<>();
        for (CollabProject p : projects) {
            if (p.getOwnerId() != null) userIds.add(p.getOwnerId());
        }
        Map<Long, User> userMap = new HashMap<>();
        for (Long uid : userIds) {
            userPort.findById(uid).ifPresent(u -> userMap.put(uid, u));
        }
        return userMap;
    }

    private ProjectMemberResponse toMemberResponse(CollabProjectMember member, User user) {
        ProjectMemberResponse resp = new ProjectMemberResponse();
        resp.setId(member.getId());
        resp.setUserId(member.getUserId());
        resp.setRole(member.getRole());
        resp.setInviteStatus(member.getInviteStatus());
        resp.setJoinedTime(member.getJoinedTime());
        if (user != null) {
            resp.setUsername(user.getUsername());
            resp.setEmail(user.getEmail());
            resp.setAvatar(user.getAvatar());
        }
        return resp;
    }

}