package com.datamate.datamanagement.application;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.datamate.common.infrastructure.exception.BusinessAssert;
import com.datamate.common.infrastructure.exception.BusinessException;
import com.datamate.common.infrastructure.exception.CommonErrorCode;
import com.datamate.common.infrastructure.exception.SystemErrorCode;
import com.datamate.common.interfaces.PagedResponse;
import com.datamate.datamanagement.common.enums.KnowledgeContentType;
import com.datamate.datamanagement.common.enums.KnowledgeSourceType;
import com.datamate.datamanagement.common.enums.KnowledgeStatusType;
import com.datamate.datamanagement.domain.model.dataset.Dataset;
import com.datamate.datamanagement.domain.model.dataset.DatasetFile;
import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItem;
import com.datamate.datamanagement.domain.model.knowledge.KnowledgeSet;
import com.datamate.datamanagement.infrastructure.config.DataManagementProperties;
import com.datamate.datamanagement.infrastructure.exception.DataManagementErrorCode;
import com.datamate.datamanagement.infrastructure.persistence.repository.DatasetFileRepository;
import com.datamate.datamanagement.infrastructure.persistence.repository.DatasetRepository;
import com.datamate.datamanagement.infrastructure.persistence.repository.KnowledgeItemRepository;
import com.datamate.datamanagement.infrastructure.persistence.repository.KnowledgeSetRepository;
import com.datamate.datamanagement.interfaces.converter.KnowledgeConverter;
import com.datamate.datamanagement.interfaces.dto.CreateKnowledgeItemRequest;
import com.datamate.datamanagement.interfaces.dto.ImportKnowledgeItemsRequest;
import com.datamate.datamanagement.interfaces.dto.KnowledgeItemPagingQuery;
import com.datamate.datamanagement.interfaces.dto.KnowledgeItemResponse;
import com.datamate.datamanagement.interfaces.dto.ReplaceKnowledgeItemFileRequest;
import com.datamate.datamanagement.interfaces.dto.UpdateKnowledgeItemRequest;
import com.datamate.datamanagement.interfaces.dto.UploadKnowledgeItemsRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

/**
 * 知识条目应用服务
 */
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class KnowledgeItemApplicationService {
    private static final Set<String> SUPPORTED_TEXT_EXTENSIONS = Set.of("txt", "md", "markdown");
    private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    private static final String EXPORT_FILE_PREFIX = "knowledge_set_";
    private static final String EXPORT_FILE_SUFFIX = ".zip";
    private static final String EXPORT_CONTENT_TYPE = "application/zip";
    private static final int MAX_FILE_BASE_LENGTH = 120;
    private static final int MAX_TITLE_LENGTH = 200;
    private static final String KNOWLEDGE_ITEM_UPLOAD_DIR = "knowledge-items";
    private static final String DEFAULT_FILE_EXTENSION = "bin";

    private final KnowledgeItemRepository knowledgeItemRepository;
    private final KnowledgeSetRepository knowledgeSetRepository;
    private final DatasetRepository datasetRepository;
    private final DatasetFileRepository datasetFileRepository;
    private final DataManagementProperties dataManagementProperties;

    public KnowledgeItem createKnowledgeItem(String setId, CreateKnowledgeItemRequest request) {
        KnowledgeSet knowledgeSet = requireKnowledgeSet(setId);
        BusinessAssert.isTrue(!isReadOnlyStatus(knowledgeSet.getStatus()),
            DataManagementErrorCode.KNOWLEDGE_SET_STATUS_ERROR);

        KnowledgeItem knowledgeItem = KnowledgeConverter.INSTANCE.convertToKnowledgeItem(request);
        knowledgeItem.setId(UUID.randomUUID().toString());
        knowledgeItem.setSetId(setId);
        if (StringUtils.isNotBlank(request.getSourceDatasetId()) || StringUtils.isNotBlank(request.getSourceFileId())) {
            knowledgeItem.setSourceType(KnowledgeSourceType.DATASET_FILE);
        } else {
            knowledgeItem.setSourceType(KnowledgeSourceType.MANUAL);
        }
        knowledgeItem.setSourceDatasetId(request.getSourceDatasetId());
        knowledgeItem.setSourceFileId(request.getSourceFileId());

        knowledgeItemRepository.save(knowledgeItem);
        return knowledgeItem;
    }

    public List<KnowledgeItem> uploadKnowledgeItems(String setId, UploadKnowledgeItemsRequest request) {
        KnowledgeSet knowledgeSet = requireKnowledgeSet(setId);
        BusinessAssert.isTrue(!isReadOnlyStatus(knowledgeSet.getStatus()),
            DataManagementErrorCode.KNOWLEDGE_SET_STATUS_ERROR);

        List<MultipartFile> files = request.getFiles();
        BusinessAssert.isTrue(CollectionUtils.isNotEmpty(files), CommonErrorCode.PARAM_ERROR);

        Path uploadRoot = resolveUploadRootPath();
        Path setDir = uploadRoot.resolve(KNOWLEDGE_ITEM_UPLOAD_DIR).resolve(setId).normalize();
        BusinessAssert.isTrue(setDir.startsWith(uploadRoot), CommonErrorCode.PARAM_ERROR);
        createDirectories(setDir);

        List<KnowledgeItem> items = new ArrayList<>();

        for (MultipartFile file : files) {
            BusinessAssert.notNull(file, CommonErrorCode.PARAM_ERROR);
            BusinessAssert.isTrue(!file.isEmpty(), CommonErrorCode.PARAM_ERROR);

            String originalName = resolveOriginalFileName(file);
            String safeOriginalName = sanitizeFileName(originalName);
            if (StringUtils.isBlank(safeOriginalName)) {
                safeOriginalName = "file";
            }

            String extension = getFileExtension(safeOriginalName);
            String storedName = UUID.randomUUID().toString() +
                (StringUtils.isBlank(extension) ? "" : "." + extension);
            Path targetPath = setDir.resolve(storedName).normalize();
            BusinessAssert.isTrue(targetPath.startsWith(setDir), CommonErrorCode.PARAM_ERROR);

            saveMultipartFile(file, targetPath);

            KnowledgeItem knowledgeItem = new KnowledgeItem();
            knowledgeItem.setId(UUID.randomUUID().toString());
            knowledgeItem.setSetId(setId);
            knowledgeItem.setContent(buildRelativeFilePath(setId, storedName));
            knowledgeItem.setContentType(KnowledgeContentType.FILE);
            knowledgeItem.setSourceType(KnowledgeSourceType.FILE_UPLOAD);
            knowledgeItem.setSourceFileId(trimToLength(safeOriginalName, MAX_TITLE_LENGTH));

            items.add(knowledgeItem);
        }

        if (CollectionUtils.isNotEmpty(items)) {
            knowledgeItemRepository.saveBatch(items, items.size());
        }
        return items;
    }

    public KnowledgeItem updateKnowledgeItem(String setId, String itemId, UpdateKnowledgeItemRequest request) {
        KnowledgeSet knowledgeSet = requireKnowledgeSet(setId);
        KnowledgeItem knowledgeItem = knowledgeItemRepository.getById(itemId);
        BusinessAssert.notNull(knowledgeItem, DataManagementErrorCode.KNOWLEDGE_ITEM_NOT_FOUND);
        BusinessAssert.isTrue(Objects.equals(knowledgeItem.getSetId(), setId), CommonErrorCode.PARAM_ERROR);
        BusinessAssert.isTrue(!isReadOnlyStatus(knowledgeSet.getStatus()),
            DataManagementErrorCode.KNOWLEDGE_SET_STATUS_ERROR);

        if (request.getContent() != null) {
            BusinessAssert.isTrue(StringUtils.isNotBlank(request.getContent()), CommonErrorCode.PARAM_ERROR);
            knowledgeItem.setContent(request.getContent());
        }
        if (request.getContentType() != null) {
            knowledgeItem.setContentType(request.getContentType());
        }

        knowledgeItemRepository.updateById(knowledgeItem);
        return knowledgeItem;
    }

    public void deleteKnowledgeItem(String setId, String itemId) {
        KnowledgeItem knowledgeItem = knowledgeItemRepository.getById(itemId);
        BusinessAssert.notNull(knowledgeItem, DataManagementErrorCode.KNOWLEDGE_ITEM_NOT_FOUND);
        BusinessAssert.isTrue(Objects.equals(knowledgeItem.getSetId(), setId), CommonErrorCode.PARAM_ERROR);
        knowledgeItemRepository.removeById(itemId);
    }

    @Transactional(readOnly = true)
    public KnowledgeItem getKnowledgeItem(String setId, String itemId) {
        KnowledgeItem knowledgeItem = knowledgeItemRepository.getById(itemId);
        BusinessAssert.notNull(knowledgeItem, DataManagementErrorCode.KNOWLEDGE_ITEM_NOT_FOUND);
        BusinessAssert.isTrue(Objects.equals(knowledgeItem.getSetId(), setId), CommonErrorCode.PARAM_ERROR);
        return knowledgeItem;
    }

    @Transactional(readOnly = true)
    public PagedResponse<KnowledgeItemResponse> getKnowledgeItems(String setId, KnowledgeItemPagingQuery query) {
        query.setSetId(setId);
        IPage<KnowledgeItem> page = new Page<>(query.getPage(), query.getSize());
        page = knowledgeItemRepository.findByCriteria(page, query);
        List<KnowledgeItemResponse> responses = KnowledgeConverter.INSTANCE.convertItemResponses(page.getRecords());
        return PagedResponse.of(responses, page.getCurrent(), page.getTotal(), page.getPages());
    }

    public List<KnowledgeItem> importKnowledgeItems(String setId, ImportKnowledgeItemsRequest request) {
        KnowledgeSet knowledgeSet = requireKnowledgeSet(setId);
        BusinessAssert.isTrue(!isReadOnlyStatus(knowledgeSet.getStatus()),
            DataManagementErrorCode.KNOWLEDGE_SET_STATUS_ERROR);
        Dataset dataset = datasetRepository.getById(request.getDatasetId());
        BusinessAssert.notNull(dataset, DataManagementErrorCode.DATASET_NOT_FOUND);

        List<KnowledgeItem> items = new ArrayList<>();
        for (String fileId : request.getFileIds()) {
            DatasetFile datasetFile = datasetFileRepository.getById(fileId);
            BusinessAssert.notNull(datasetFile, DataManagementErrorCode.DATASET_FILE_NOT_FOUND);
            BusinessAssert.isTrue(Objects.equals(datasetFile.getDatasetId(), dataset.getId()), CommonErrorCode.PARAM_ERROR);

            KnowledgeContentType contentType = resolveContentType(datasetFile);
            String content = readTextContent(datasetFile, dataset);

            KnowledgeItem knowledgeItem = new KnowledgeItem();
            knowledgeItem.setId(UUID.randomUUID().toString());
            knowledgeItem.setSetId(setId);
            knowledgeItem.setContent(content);
            knowledgeItem.setContentType(contentType);
            knowledgeItem.setSourceType(KnowledgeSourceType.DATASET_FILE);
            knowledgeItem.setSourceDatasetId(dataset.getId());
            knowledgeItem.setSourceFileId(datasetFile.getId());

            items.add(knowledgeItem);
        }

        if (CollectionUtils.isNotEmpty(items)) {
            knowledgeItemRepository.saveBatch(items, items.size());
        }
        return items;
    }

    @Transactional(readOnly = true)
    public void exportKnowledgeItems(String setId, HttpServletResponse response) {
        BusinessAssert.notNull(response, CommonErrorCode.PARAM_ERROR);
        KnowledgeSet knowledgeSet = requireKnowledgeSet(setId);
        List<KnowledgeItem> items = knowledgeItemRepository.findAllBySetId(setId);

        response.setContentType(EXPORT_CONTENT_TYPE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
            "attachment; filename=\"" + buildExportFileName(knowledgeSet.getId()) + "\"");

        try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(response.getOutputStream())) {
            zos.setEncoding(StandardCharsets.UTF_8.name());
            Map<String, Integer> nameCounter = new HashMap<>();
            for (KnowledgeItem item : items) {
                String entryName = buildItemEntryName(item, nameCounter);
                ZipArchiveEntry entry = new ZipArchiveEntry(entryName);
                zos.putArchiveEntry(entry);
                byte[] contentBytes = resolveExportContent(item);
                zos.write(contentBytes);
                zos.closeArchiveEntry();
            }
            zos.finish();
        } catch (IOException e) {
            log.error("export knowledge items error, setId: {}", setId, e);
            throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
        }
    }

    @Transactional(readOnly = true)
    public void downloadKnowledgeItemFile(String setId, String itemId, HttpServletResponse response) {
        BusinessAssert.notNull(response, CommonErrorCode.PARAM_ERROR);
        KnowledgeItem knowledgeItem = knowledgeItemRepository.getById(itemId);
        BusinessAssert.notNull(knowledgeItem, DataManagementErrorCode.KNOWLEDGE_ITEM_NOT_FOUND);
        BusinessAssert.isTrue(Objects.equals(knowledgeItem.getSetId(), setId), CommonErrorCode.PARAM_ERROR);
        BusinessAssert.isTrue(knowledgeItem.getContentType() == KnowledgeContentType.FILE
                || knowledgeItem.getSourceType() == KnowledgeSourceType.FILE_UPLOAD,
            CommonErrorCode.PARAM_ERROR);

        String relativePath = knowledgeItem.getContent();
        BusinessAssert.isTrue(StringUtils.isNotBlank(relativePath), CommonErrorCode.PARAM_ERROR);
        Path filePath = resolveKnowledgeItemStoragePath(relativePath);
        BusinessAssert.isTrue(Files.exists(filePath) && Files.isRegularFile(filePath), CommonErrorCode.PARAM_ERROR);

        String downloadName = StringUtils.isNotBlank(knowledgeItem.getSourceFileId())
            ? knowledgeItem.getSourceFileId()
            : filePath.getFileName().toString();

        response.setContentType("application/octet-stream");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
            "attachment; filename=\"" + URLEncoder.encode(downloadName, StandardCharsets.UTF_8) + "\"");

        try (InputStream inputStream = Files.newInputStream(filePath)) {
            inputStream.transferTo(response.getOutputStream());
            response.flushBuffer();
        } catch (IOException e) {
            log.error("download knowledge item file error, itemId: {}", itemId, e);
            throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
        }
    }

    @Transactional(readOnly = true)
    public void previewKnowledgeItemFile(String setId, String itemId, HttpServletResponse response) {
        BusinessAssert.notNull(response, CommonErrorCode.PARAM_ERROR);
        KnowledgeItem knowledgeItem = knowledgeItemRepository.getById(itemId);
        BusinessAssert.notNull(knowledgeItem, DataManagementErrorCode.KNOWLEDGE_ITEM_NOT_FOUND);
        BusinessAssert.isTrue(Objects.equals(knowledgeItem.getSetId(), setId), CommonErrorCode.PARAM_ERROR);
        BusinessAssert.isTrue(knowledgeItem.getContentType() == KnowledgeContentType.FILE
                || knowledgeItem.getSourceType() == KnowledgeSourceType.FILE_UPLOAD,
            CommonErrorCode.PARAM_ERROR);

        String relativePath = knowledgeItem.getContent();
        BusinessAssert.isTrue(StringUtils.isNotBlank(relativePath), CommonErrorCode.PARAM_ERROR);
        Path filePath = resolveKnowledgeItemStoragePath(relativePath);
        BusinessAssert.isTrue(Files.exists(filePath) && Files.isRegularFile(filePath), CommonErrorCode.PARAM_ERROR);

        String previewName = StringUtils.isNotBlank(knowledgeItem.getSourceFileId())
            ? knowledgeItem.getSourceFileId()
            : filePath.getFileName().toString();

        String contentType = null;
        try {
            contentType = Files.probeContentType(filePath);
        } catch (IOException e) {
            log.warn("probe knowledge item file content type failed, itemId: {}", itemId, e);
        }
        if (StringUtils.isBlank(contentType)) {
            contentType = "application/octet-stream";
        }

        response.setContentType(contentType);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
            "inline; filename=\"" + URLEncoder.encode(previewName, StandardCharsets.UTF_8) + "\"");

        try (InputStream inputStream = Files.newInputStream(filePath)) {
            inputStream.transferTo(response.getOutputStream());
            response.flushBuffer();
        } catch (IOException e) {
            log.error("preview knowledge item file error, itemId: {}", itemId, e);
            throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
        }
    }

    public KnowledgeItem replaceKnowledgeItemFile(String setId, String itemId, ReplaceKnowledgeItemFileRequest request) {
        KnowledgeSet knowledgeSet = requireKnowledgeSet(setId);
        KnowledgeItem knowledgeItem = knowledgeItemRepository.getById(itemId);
        BusinessAssert.notNull(knowledgeItem, DataManagementErrorCode.KNOWLEDGE_ITEM_NOT_FOUND);
        BusinessAssert.isTrue(Objects.equals(knowledgeItem.getSetId(), setId), CommonErrorCode.PARAM_ERROR);
        BusinessAssert.isTrue(!isReadOnlyStatus(knowledgeSet.getStatus()),
            DataManagementErrorCode.KNOWLEDGE_SET_STATUS_ERROR);

        MultipartFile file = request == null ? null : request.getFile();
        BusinessAssert.notNull(file, CommonErrorCode.PARAM_ERROR);
        BusinessAssert.isTrue(!file.isEmpty(), CommonErrorCode.PARAM_ERROR);
        BusinessAssert.isTrue(knowledgeItem.getContentType() == KnowledgeContentType.FILE
                || knowledgeItem.getSourceType() == KnowledgeSourceType.FILE_UPLOAD,
            CommonErrorCode.PARAM_ERROR);

        String oldRelativePath = knowledgeItem.getContent();
        BusinessAssert.isTrue(StringUtils.isNotBlank(oldRelativePath), CommonErrorCode.PARAM_ERROR);
        Path oldFilePath = resolveKnowledgeItemStoragePath(oldRelativePath);
        BusinessAssert.isTrue(Files.exists(oldFilePath) && Files.isRegularFile(oldFilePath), CommonErrorCode.PARAM_ERROR);

        Path uploadRoot = resolveUploadRootPath();
        Path setDir = uploadRoot.resolve(KNOWLEDGE_ITEM_UPLOAD_DIR).resolve(setId).normalize();
        BusinessAssert.isTrue(setDir.startsWith(uploadRoot), CommonErrorCode.PARAM_ERROR);
        createDirectories(setDir);

        String originalName = resolveOriginalFileName(file);
        String safeOriginalName = sanitizeFileName(originalName);
        if (StringUtils.isBlank(safeOriginalName)) {
            safeOriginalName = "file";
        }

        String extension = getFileExtension(safeOriginalName);
        String storedName = UUID.randomUUID().toString() +
            (StringUtils.isBlank(extension) ? "" : "." + extension);
        Path targetPath = setDir.resolve(storedName).normalize();
        BusinessAssert.isTrue(targetPath.startsWith(setDir), CommonErrorCode.PARAM_ERROR);

        saveMultipartFile(file, targetPath);

        String sourceFileId = trimToLength(safeOriginalName, MAX_TITLE_LENGTH);
        String newRelativePath = buildRelativeFilePath(setId, storedName);

        try {
            knowledgeItem.setContent(newRelativePath);
            knowledgeItem.setContentType(KnowledgeContentType.FILE);
            knowledgeItem.setSourceType(KnowledgeSourceType.FILE_UPLOAD);
            knowledgeItem.setSourceFileId(sourceFileId);
            knowledgeItemRepository.updateById(knowledgeItem);
            deleteFile(oldFilePath);
        } catch (Exception e) {
            deleteFileQuietly(targetPath);
            if (e instanceof BusinessException) {
                throw (BusinessException) e;
            }
            throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
        }

        return knowledgeItem;
    }

    private byte[] resolveExportContent(KnowledgeItem item) {
        if (item.getContentType() == KnowledgeContentType.FILE) {
            String relativePath = item.getContent();
            BusinessAssert.isTrue(StringUtils.isNotBlank(relativePath), CommonErrorCode.PARAM_ERROR);
            Path filePath = resolveKnowledgeItemStoragePath(relativePath);
            BusinessAssert.isTrue(Files.exists(filePath) && Files.isRegularFile(filePath), CommonErrorCode.PARAM_ERROR);
            try {
                return Files.readAllBytes(filePath);
            } catch (IOException e) {
                log.error("export knowledge item file error, itemId: {}", item.getId(), e);
                throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
            }
        }
        return (item.getContent() == null ? "" : item.getContent()).getBytes(StandardCharsets.UTF_8);
    }

    private String resolveExportExtension(KnowledgeItem item) {
        if (item.getContentType() == KnowledgeContentType.MARKDOWN) {
            return "md";
        }
        if (item.getContentType() == KnowledgeContentType.FILE) {
            String extension = getFileExtension(item.getSourceFileId());
            if (StringUtils.isBlank(extension)) {
                extension = getFileExtensionFromPath(item.getContent());
            }
            return StringUtils.isBlank(extension) ? DEFAULT_FILE_EXTENSION : extension;
        }
        return "txt";
    }

    private String getFileExtensionFromPath(String path) {
        if (StringUtils.isBlank(path)) {
            return "";
        }
        Path fileName = Paths.get(path).getFileName();
        return fileName == null ? "" : getFileExtension(fileName.toString());
    }

    private Path resolveUploadRootPath() {
        String uploadDir = dataManagementProperties.getFileStorage().getUploadDir();
        BusinessAssert.isTrue(StringUtils.isNotBlank(uploadDir), CommonErrorCode.PARAM_ERROR);
        return Paths.get(uploadDir).toAbsolutePath().normalize();
    }

    private Path resolveKnowledgeItemStoragePath(String relativePath) {
        String normalizedRelativePath = relativePath.replace("/", File.separator);
        Path root = resolveUploadRootPath();
        Path target = root.resolve(normalizedRelativePath).toAbsolutePath().normalize();
        BusinessAssert.isTrue(target.startsWith(root), CommonErrorCode.PARAM_ERROR);
        return target;
    }

    private String buildRelativeFilePath(String setId, String storedName) {
        String relativePath = Paths.get(KNOWLEDGE_ITEM_UPLOAD_DIR, setId, storedName).toString();
        return relativePath.replace(File.separatorChar, '/');
    }

    private void createDirectories(Path path) {
        try {
            Files.createDirectories(path);
        } catch (IOException e) {
            log.error("create knowledge item upload dir error, path: {}", path, e);
            throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
        }
    }

    private void saveMultipartFile(MultipartFile file, Path targetPath) {
        try (InputStream inputStream = file.getInputStream()) {
            Files.copy(inputStream, targetPath);
        } catch (IOException e) {
            log.error("save knowledge item file error, target: {}", targetPath, e);
            throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
        }
    }

    private void deleteFile(Path filePath) {
        try {
            Files.deleteIfExists(filePath);
        } catch (IOException e) {
            log.error("delete knowledge item file error, path: {}", filePath, e);
            throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
        }
    }

    private void deleteFileQuietly(Path filePath) {
        try {
            Files.deleteIfExists(filePath);
        } catch (IOException e) {
            log.warn("delete knowledge item file quietly error, path: {}", filePath, e);
        }
    }

    private String resolveOriginalFileName(MultipartFile file) {
        String originalName = file.getOriginalFilename();
        if (StringUtils.isBlank(originalName)) {
            originalName = file.getName();
        }
        if (originalName == null) {
            return "";
        }
        Path fileName = Paths.get(originalName).getFileName();
        return fileName == null ? originalName : fileName.toString();
    }

    private KnowledgeSet requireKnowledgeSet(String setId) {
        KnowledgeSet knowledgeSet = knowledgeSetRepository.getById(setId);
        BusinessAssert.notNull(knowledgeSet, DataManagementErrorCode.KNOWLEDGE_SET_NOT_FOUND);
        return knowledgeSet;
    }

    private String buildExportFileName(String setId) {
        return EXPORT_FILE_PREFIX + setId + "_" + LocalDateTime.now().format(EXPORT_TIME_FORMATTER) + EXPORT_FILE_SUFFIX;
    }

    private String buildItemEntryName(KnowledgeItem item, Map<String, Integer> nameCounter) {
        String rawName = StringUtils.isNotBlank(item.getSourceFileId())
            ? stripExtension(item.getSourceFileId())
            : "item-" + item.getId();
        String baseName = sanitizeFileName(rawName);
        if (StringUtils.isBlank(baseName)) {
            baseName = "item-" + item.getId();
        }
        baseName = trimToLength(baseName, MAX_FILE_BASE_LENGTH);

        String extension = resolveExportExtension(item);
        String baseKey = baseName + "." + extension;
        int count = nameCounter.getOrDefault(baseKey, 0);
        nameCounter.put(baseKey, count + 1);
        if (count == 0) {
            return baseKey;
        }
        return buildIndexedFileName(baseName, extension, count);
    }

    private String buildIndexedFileName(String baseName, String extension, int index) {
        String suffix = "_" + index;
        int maxBaseLength = Math.max(1, MAX_FILE_BASE_LENGTH - suffix.length());
        String safeBase = trimToLength(baseName, maxBaseLength);
        return safeBase + suffix + "." + extension;
    }

    private String sanitizeFileName(String name) {
        if (name == null) {
            return "";
        }
        String normalized = name.replaceAll("[\\\\/:*?\"<>|]", "_");
        normalized = normalized.replaceAll("\\s+", " ").trim();
        return normalized;
    }

    private String trimToLength(String value, int maxLength) {
        if (value == null) {
            return "";
        }
        if (value.length() <= maxLength) {
            return value;
        }
        return value.substring(0, maxLength);
    }

    private KnowledgeContentType resolveContentType(DatasetFile datasetFile) {
        String extension = getFileExtension(datasetFile);
        if (StringUtils.isBlank(extension)) {
            throw BusinessException.of(DataManagementErrorCode.KNOWLEDGE_ITEM_SOURCE_NOT_SUPPORTED);
        }
        String normalized = extension.toLowerCase();
        if (!SUPPORTED_TEXT_EXTENSIONS.contains(normalized)) {
            throw BusinessException.of(DataManagementErrorCode.KNOWLEDGE_ITEM_SOURCE_NOT_SUPPORTED);
        }
        return normalized.equals("md") || normalized.equals("markdown")
            ? KnowledgeContentType.MARKDOWN
            : KnowledgeContentType.TEXT;
    }

    private String getFileExtension(String fileName) {
        if (StringUtils.isBlank(fileName)) {
            return "";
        }
        int dotIndex = fileName.lastIndexOf('.');
        if (dotIndex <= 0 || dotIndex >= fileName.length() - 1) {
            return "";
        }
        return fileName.substring(dotIndex + 1);
    }

    private String getFileExtension(DatasetFile datasetFile) {
        String fileName = datasetFile.getFileName();
        if (StringUtils.isNotBlank(fileName) && fileName.contains(".")) {
            return fileName.substring(fileName.lastIndexOf('.') + 1);
        }
        String fileType = datasetFile.getFileType();
        if (StringUtils.isNotBlank(fileType) && fileType.startsWith(".")) {
            return fileType.substring(1);
        }
        return fileType;
    }

    private String readTextContent(DatasetFile datasetFile, Dataset dataset) {
        Path filePath = Paths.get(datasetFile.getFilePath()).normalize();
        Path datasetPath = Paths.get(dataset.getPath()).normalize();
        BusinessAssert.isTrue(filePath.startsWith(datasetPath), CommonErrorCode.PARAM_ERROR);
        BusinessAssert.isTrue(Files.exists(filePath) && Files.isRegularFile(filePath), CommonErrorCode.PARAM_ERROR);
        try {
            return Files.readString(filePath, StandardCharsets.UTF_8);
        } catch (IOException e) {
            log.error("read knowledge content error, fileId: {}", datasetFile.getId(), e);
            throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
        }
    }

    private String stripExtension(String fileName) {
        if (StringUtils.isBlank(fileName)) {
            return "未命名";
        }
        int dotIndex = fileName.lastIndexOf('.');
        if (dotIndex <= 0) {
            return fileName;
        }
        return fileName.substring(0, dotIndex);
    }

    private boolean isReadOnlyStatus(KnowledgeStatusType status) {
        return status == KnowledgeStatusType.ARCHIVED || status == KnowledgeStatusType.DEPRECATED;
    }
}
