package com.agent.management.service.impl;

import com.agent.management.common.exception.BusinessException;
import com.agent.management.config.SkillProperties;
import com.agent.management.engine.hermes.HermesSkillSyncService;
import com.agent.management.model.dto.FileCreateDTO;
import com.agent.management.model.dto.FileNodeDTO;
import com.agent.management.model.dto.IOField;
import com.agent.management.model.dto.SkillDTO;
import com.agent.management.model.dto.SkillUpdateDTO;
import com.agent.management.model.entity.Workflow;
import com.agent.management.model.vo.SkillDetailVO;
import com.agent.management.parser.SkillMarkdownParser;
import com.agent.management.parser.SkillNameNormalizer;
import com.agent.management.repository.SkillCategoryRepository;
import com.agent.management.repository.WorkflowRepository;
import com.agent.management.service.SkillService;
import com.agent.management.service.SkillVersionService;
import com.agent.management.service.TagService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@Service
@RequiredArgsConstructor
public class SkillServiceImpl implements SkillService {

    private final SkillProperties skillProperties;
    private final SkillMarkdownParser skillMarkdownParser;
    private final SkillVersionService skillVersionService;
    private final TagService tagService;
    private final SkillCategoryRepository skillCategoryRepository;
    private final WorkflowRepository workflowRepository;
    private final HermesSkillSyncService hermesSkillSyncService;

    private static final String SKILL_MD_FILENAME = "SKILL.md";
    private static final String VERSIONS_DIR = "_versions";
    private static final String VERSION_META_FILE = "_version.json";

    @Override
    public List<SkillDTO> listSkills() {
        Path basePath = getBasePath();
        List<SkillDTO> skills = new ArrayList<>();

        try (Stream<Path> paths = Files.list(basePath)) {
            paths.filter(Files::isDirectory)
                 .forEach(dir -> {
                     Path skillMd = dir.resolve(SKILL_MD_FILENAME);
                     if (Files.exists(skillMd)) {
                         skillMarkdownParser.parse(skillMd).ifPresent(skills::add);
                     }
                 });
        } catch (IOException e) {
            log.error("扫描 Skills 目录失败: {}", basePath, e);
            throw new BusinessException("读取 Skills 目录失败");
        }

        return skills;
    }

    @Override
    public SkillDTO getSkill(String folderName) {
        Path skillDir = resolveSkillDir(folderName);
        if (!Files.exists(skillDir) || !Files.isDirectory(skillDir)) {
            throw new BusinessException(404, "Skill 不存在: " + folderName);
        }

        Path skillMd = skillDir.resolve(SKILL_MD_FILENAME);
        if (!Files.exists(skillMd)) {
            throw new BusinessException(404, "Skill 文件不存在: " + folderName);
        }

        return skillMarkdownParser.parse(skillMd)
                .orElseThrow(() -> new BusinessException(422, "SKILL.md 格式不正确: " + folderName));
    }

    @Override
    public void createSkill(MultipartFile zipFile) {
        if (zipFile == null || zipFile.isEmpty()) {
            throw new BusinessException("上传文件不能为空");
        }

        String originalFilename = zipFile.getOriginalFilename();
        if (originalFilename == null || (!originalFilename.toLowerCase().endsWith(".zip"))) {
            throw new BusinessException("仅支持 .zip 格式的压缩文件");
        }

        Path tempDir = null;
        try {
            // 在临时目录中解压
            tempDir = Files.createTempDirectory("skill-upload-");
            Path zipPath = tempDir.resolve("upload.zip");
            zipFile.transferTo(zipPath.toFile());

            // 解压 zip 文件
            extractZip(zipPath, tempDir.resolve("extracted"));

            Path extractedDir = tempDir.resolve("extracted");

            // 验证解压内容：第一层目录中包含 SKILL.md
            Path skillDir = findFirstSkillDirectory(extractedDir);
            if (skillDir == null) {
                throw new BusinessException(422, "压缩包格式不正确：未找到包含 SKILL.md 的目录");
            }

            // 验证 SKILL.md 可解析
            Path skillMd = skillDir.resolve(SKILL_MD_FILENAME);
            SkillDTO parsed = skillMarkdownParser.parse(skillMd)
                    .orElseThrow(() -> new BusinessException(422, "SKILL.md 格式不正确：无法解析 name 或 description"));

            // 用规范化的 name 作为目标目录名（kebab-case 严格校验）
            // 不信任 zip 内目录名，避免大写/下划线/中文导致的引用不一致
            String folderName = SkillNameNormalizer.normalizeStrict(parsed.getName());
            Path targetDir = resolveSkillDir(folderName);
            if (Files.exists(targetDir)) {
                throw new BusinessException(409, "同名 Skill 已存在: " + folderName);
            }

            FileUtils.copyDirectory(skillDir.toFile(), targetDir.toFile());

            // 上传后自动创建 v1 快照
            skillVersionService.createSnapshot(folderName, "初始版本");
            log.info("新增 Skill 成功: {}", folderName);
            hermesSkillSyncService.syncAfterWrite();

        } catch (BusinessException e) {
            throw e;
        } catch (IOException e) {
            log.error("上传 Skill 失败", e);
            throw new BusinessException("上传处理失败: " + e.getMessage());
        } finally {
            // 清理临时目录
            if (tempDir != null) {
                try {
                    FileUtils.deleteDirectory(tempDir.toFile());
                } catch (IOException e) {
                    log.warn("清理临时目录失败: {}", tempDir, e);
                }
            }
        }
    }

    private static final ObjectMapper jsonMapper = new ObjectMapper();

    @Override
    public String updateSkill(String folderName, SkillUpdateDTO dto) {
        // 1. 规范化 name（若提供）。若与 folderName 不同则触发重命名 + 副作用迁移
        String effectiveFolder = folderName;
        if (dto.getName() != null && !dto.getName().isEmpty()) {
            String normalized = SkillNameNormalizer.normalizeStrict(dto.getName());
            if (!normalized.equals(folderName)) {
                try {
                    effectiveFolder = renameSkillFolderWithSideEffects(folderName, normalized);
                } catch (IOException e) {
                    log.error("重命名 Skill 失败: {} -> {}", folderName, normalized, e);
                    throw new BusinessException("重命名 Skill 失败");
                }
            }
            dto.setName(normalized);
        }

        Path skillDir = resolveSkillDir(effectiveFolder);
        if (!Files.exists(skillDir) || !Files.isDirectory(skillDir)) {
            throw new BusinessException(404, "Skill 不存在: " + effectiveFolder);
        }

        Path skillMd = skillDir.resolve(SKILL_MD_FILENAME);
        if (!Files.exists(skillMd)) {
            throw new BusinessException(404, "SKILL.md 文件不存在");
        }

        try {
            String content = Files.readString(skillMd, StandardCharsets.UTF_8);
            String[] lines = content.split("\n");
            List<String> result = new ArrayList<>();
            boolean nameUpdated = false;
            boolean displayNameUpdated = false;
            boolean descUpdated = false;
            boolean inputsUpdated = false;
            boolean outputsUpdated = false;
            boolean canGenUpdated = false;

            for (String line : lines) {
                String trimmed = line.trim().toLowerCase();
                if (!nameUpdated && trimmed.startsWith("name:")) {
                    result.add("name: " + dto.getName());
                    nameUpdated = true;
                    continue;
                }
                if (!displayNameUpdated && trimmed.startsWith("displayname:")) {
                    if (dto.getDisplayName() != null) {
                        result.add("displayName: " + dto.getDisplayName());
                    }
                    displayNameUpdated = true;
                    continue;
                }
                if (!descUpdated && trimmed.startsWith("description:")) {
                    result.add("description: " + dto.getDescription());
                    descUpdated = true;
                    // 如果 inputs/outputs 需要新增，紧随其后插入
                    if (dto.getInputs() != null) {
                        result.add("inputs: " + jsonMapper.writeValueAsString(dto.getInputs()));
                        inputsUpdated = true;
                    }
                    if (dto.getOutputs() != null) {
                        result.add("outputs: " + jsonMapper.writeValueAsString(dto.getOutputs()));
                        outputsUpdated = true;
                    }
                    continue;
                }
                if (!inputsUpdated && trimmed.startsWith("inputs:")) {
                    if (dto.getInputs() != null) {
                        result.add("inputs: " + jsonMapper.writeValueAsString(dto.getInputs()));
                    }
                    inputsUpdated = true;
                    continue;
                }
                if (!outputsUpdated && trimmed.startsWith("outputs:")) {
                    if (dto.getOutputs() != null) {
                        result.add("outputs: " + jsonMapper.writeValueAsString(dto.getOutputs()));
                    }
                    outputsUpdated = true;
                    continue;
                }
                if (!canGenUpdated && trimmed.startsWith("cangenerateskill:")) {
                    if (dto.getCanGenerateSkill() != null) {
                        result.add("canGenerateSkill: " + dto.getCanGenerateSkill());
                    }
                    canGenUpdated = true;
                    continue;
                }
                result.add(line);
            }

            // 如果原文件中没有 name/description 字段，追加到开头
            if (!nameUpdated) {
                result.add(0, "name: " + dto.getName());
            }
            if (!displayNameUpdated && dto.getDisplayName() != null) {
                // 紧随 name 之后插入 displayName
                int idx = 0;
                for (int i = 0; i < result.size(); i++) {
                    if (result.get(i).trim().toLowerCase().startsWith("name:")) {
                        idx = i + 1;
                        break;
                    }
                }
                result.add(idx, "displayName: " + dto.getDisplayName());
            }
            if (!descUpdated) {
                result.add(result.isEmpty() ? 0 : 1, "description: " + dto.getDescription());
                // description 后追加 inputs/outputs
                int insertAt = result.indexOf("description: " + dto.getDescription()) + 1;
                if (dto.getInputs() != null) {
                    result.add(insertAt, "inputs: " + jsonMapper.writeValueAsString(dto.getInputs()));
                    insertAt++;
                    inputsUpdated = true;
                }
                if (dto.getOutputs() != null) {
                    result.add(insertAt, "outputs: " + jsonMapper.writeValueAsString(dto.getOutputs()));
                    outputsUpdated = true;
                }
            }
            // canGenerateSkill 显式提供但原文件没有，追加到末尾
            if (!canGenUpdated && dto.getCanGenerateSkill() != null) {
                result.add("canGenerateSkill: " + dto.getCanGenerateSkill());
            }

            String updated = String.join("\n", result);
            if (!content.endsWith("\n") && updated.endsWith("\n")) {
                updated = updated.substring(0, updated.length() - 1);
            }
            Files.writeString(skillMd, updated, StandardCharsets.UTF_8);
            log.info("更新 Skill 成功: {} (renamed={})", effectiveFolder, !effectiveFolder.equals(folderName));
            hermesSkillSyncService.syncAfterWrite();

        } catch (IOException e) {
            log.error("更新 SKILL.md 失败: {}", skillMd, e);
            throw new BusinessException("更新 SKILL.md 失败");
        }
        return effectiveFolder;
    }

    /**
     * 历史数据迁移：扫描全部 skill，将 folderName 与 SKILL.md name（kebab-case 规范化后）
     * 不一致的统一重命名，复用 {@link #renameSkillFolderWithSideEffects} 完成全部副作用迁移。
     * 单条失败不阻塞整体迁移。
     */
    @Override
    public Map<String, Object> migrateAllFoldersNormalize() {
        List<SkillDTO> skills = listSkills();
        List<Map<String, String>> migrated = new ArrayList<>();
        List<Map<String, String>> failed = new ArrayList<>();
        List<String> skipped = new ArrayList<>();

        for (SkillDTO skill : skills) {
            String folder = skill.getFolderName();
            String name = skill.getName();
            if (name == null || name.isEmpty()) {
                skipped.add(folder + " (name 为空)");
                continue;
            }
            String normalized = SkillNameNormalizer.normalize(name);
            if (!SkillNameNormalizer.isValidKebabCase(normalized)) {
                failed.add(Map.of("folder", folder, "reason",
                        "规范化后非法: \"" + normalized + "\"（可能含中文），请手动修复"));
                continue;
            }
            if (normalized.equals(folder)) {
                skipped.add(folder);
                continue;
            }
            try {
                renameSkillFolderWithSideEffects(folder, normalized);
                migrated.add(Map.of("from", folder, "to", normalized));
            } catch (Exception e) {
                failed.add(Map.of("folder", folder, "reason",
                        e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()));
            }
        }
        log.info("[Skill-Migrate] 总数={}, 已迁移={}, 跳过={}, 失败={}",
                skills.size(), migrated.size(), skipped.size(), failed.size());
        if (!migrated.isEmpty()) {
            hermesSkillSyncService.syncAfterWrite();
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("total", skills.size());
        result.put("migrated", migrated);
        result.put("failed", failed);
        result.put("skipped", skipped);
        return result;
    }

    @Override
    public void deleteSkill(String folderName) {
        Path skillDir = resolveSkillDir(folderName);
        if (!Files.exists(skillDir) || !Files.isDirectory(skillDir)) {
            throw new BusinessException(404, "Skill 不存在: " + folderName);
        }

        try {
            FileUtils.deleteDirectory(skillDir.toFile());
            log.info("删除 Skill 成功: {}", folderName);
            hermesSkillSyncService.syncAfterWrite();
        } catch (IOException e) {
            log.error("删除 Skill 失败: {}", skillDir, e);
            throw new BusinessException("删除 Skill 失败");
        }
    }

    @Override
    public String getSkillFullContent(String folderName) {
        Path skillMd = resolveSkillDir(folderName).resolve(SKILL_MD_FILENAME);
        if (!Files.exists(skillMd)) {
            throw new BusinessException(404, "SKILL.md 文件不存在: " + folderName);
        }
        try {
            return Files.readString(skillMd, StandardCharsets.UTF_8);
        } catch (IOException e) {
            log.error("读取 SKILL.md 内容失败: {}", skillMd, e);
            throw new BusinessException("读取 SKILL.md 内容失败");
        }
    }

    @Override
    public SkillDetailVO getSkillDetail(String folderName) {
        SkillDTO skill = getSkill(folderName);
        String fullContent = getSkillFullContent(folderName);
        String body = extractBody(fullContent);

        SkillDetailVO detail = new SkillDetailVO();
        detail.setFolderName(skill.getFolderName());
        detail.setName(skill.getName());
        detail.setDisplayName(skill.getDisplayName());
        detail.setDescription(skill.getDescription());
        detail.setCanGenerateSkill(skill.isCanGenerateSkill());
        detail.setInputs(skill.getInputs());
        detail.setOutputs(skill.getOutputs());
        detail.setBody(body);
        return detail;
    }

    @Override
    public String saveSkillMd(String folderName, String name, String displayName,
                              String description,
                              boolean canGenerateSkill,
                              List<IOField> inputs, List<IOField> outputs, String body) {
        // 1. 规范化为 kebab-case（若非法抛 BusinessException，无副作用）
        String normalized = SkillNameNormalizer.normalizeStrict(name);
        String effectiveFolder = folderName;

        try {
            // 2. name 变更时重命名目录 + 迁移副作用引用
            if (!normalized.equals(folderName)) {
                effectiveFolder = renameSkillFolderWithSideEffects(folderName, normalized);
            }

            // 3. 写入新位置的 SKILL.md（使用 normalized 作为 name 字段）
            Path skillMd = resolveSkillPath(effectiveFolder, SKILL_MD_FILENAME);
            String original = Files.exists(skillMd)
                    ? Files.readString(skillMd, StandardCharsets.UTF_8)
                    : "";

            String merged = mergeSkillMd(original, normalized, displayName, description,
                    canGenerateSkill, inputs, outputs, body);
            Files.writeString(skillMd, merged, StandardCharsets.UTF_8);

            // 4. 保存后自动创建快照
            skillVersionService.createSnapshot(effectiveFolder, null);
            log.info("保存 SKILL.md 成功: {} (renamed={})", effectiveFolder, !normalized.equals(folderName));
            hermesSkillSyncService.syncAfterWrite();
            return effectiveFolder;
        } catch (BusinessException e) {
            throw e;
        } catch (IOException e) {
            log.error("保存 SKILL.md 失败", e);
            throw new BusinessException("保存 SKILL.md 失败");
        }
    }

    /**
     * 重命名 skill 目录并同步迁移所有副作用引用。
     * - 物理目录 Files.move（_versions 随之迁移）
     * - 标签关联（TagAssignment.entityId）
     * - 分类关联（SkillCategory.skillFolderName）
     * - 工作流节点引用（graphData JSON 中的 data.skillId）
     *
     * <p>副作用迁移部分失败时仅记录 WARN，不阻塞主流程；
     * 物理重命名失败则抛 BusinessException。
     *
     * @return 新 folderName
     */
    private String renameSkillFolderWithSideEffects(String oldFolder, String newFolder) throws IOException {
        Path oldDir = resolveSkillDir(oldFolder);
        Path newDir = resolveSkillDir(newFolder);
        if (!Files.exists(oldDir)) {
            throw new BusinessException(404, "Skill 不存在: " + oldFolder);
        }
        if (Files.exists(newDir)) {
            throw new BusinessException(409, "目标名称已存在: " + newFolder);
        }

        // 1. 物理重命名（_versions 会随之移动）
        Files.move(oldDir, newDir);

        // 2. 副作用迁移（部分失败不阻塞，仅日志）
        try {
            int affected = tagService.renameEntityId("skill", oldFolder, newFolder);
            if (affected > 0) log.info("[Skill] 标签关联迁移: {} -> {} (affected={})", oldFolder, newFolder, affected);
        } catch (Exception e) {
            log.warn("[Skill] 标签关联迁移失败: {} -> {}", oldFolder, newFolder, e);
        }
        try {
            int affected = skillCategoryRepository.renameSkillFolderName(oldFolder, newFolder);
            if (affected > 0) log.info("[Skill] 分类关联迁移: {} -> {} (affected={})", oldFolder, newFolder, affected);
        } catch (Exception e) {
            log.warn("[Skill] 分类关联迁移失败: {} -> {}", oldFolder, newFolder, e);
        }
        try {
            int affected = migrateWorkflowSkillReferences(oldFolder, newFolder);
            if (affected > 0) log.info("[Skill] 工作流引用迁移: {} -> {} (affected={})", oldFolder, newFolder, affected);
        } catch (Exception e) {
            log.warn("[Skill] 工作流引用迁移失败: {} -> {}", oldFolder, newFolder, e);
        }
        log.info("[Skill] 重命名完成: {} -> {}", oldFolder, newFolder);
        return newFolder;
    }

    /**
     * 扫描所有 workflow 的 graphData，把 data.skillId == oldFolder 的引用替换为 newFolder。
     * 使用 Jackson 解析，避免字符串替换误伤其他字段。
     *
     * @return 受影响的工作流数量
     */
    private int migrateWorkflowSkillReferences(String oldFolder, String newFolder) {
        int affected = 0;
        for (Workflow wf : workflowRepository.findAll()) {
            String graphData = wf.getGraphData();
            if (graphData == null || graphData.isEmpty() || !graphData.contains(oldFolder)) continue;
            try {
                JsonNode root = jsonMapper.readTree(graphData);
                JsonNode nodes = root.get("nodes");
                boolean changed = false;
                if (nodes != null && nodes.isArray()) {
                    for (JsonNode node : nodes) {
                        JsonNode data = node.get("data");
                        if (data == null || !data.has("skillId")) continue;
                        JsonNode skillIdNode = data.get("skillId");
                        if (skillIdNode != null && skillIdNode.isTextual()
                                && oldFolder.equals(skillIdNode.asText())) {
                            ((ObjectNode) data).put("skillId", newFolder);
                            changed = true;
                        }
                    }
                }
                if (changed) {
                    wf.setGraphData(jsonMapper.writeValueAsString(root));
                    wf.onUpdate();
                    workflowRepository.save(wf);
                    affected++;
                }
            } catch (Exception e) {
                log.warn("[Skill] 工作流 {} graphData 解析失败，跳过 skillId 迁移", wf.getId(), e);
            }
        }
        return affected;
    }

    /**
     * 智能合并 SKILL.md 内容
     * - 保留 frontmatter 中的非标准字段（如 version, source 等）
     * - 替换/新增 name, displayName, description, canGenerateSkill, inputs, outputs
     * - 无 frontmatter 时自动包装
     */
    private String mergeSkillMd(String original, String name, String displayName,
                                String description,
                                boolean canGenerateSkill,
                                List<IOField> inputs, List<IOField> outputs, String body) {
        String[] lines = original.split("\n");
        List<String> metaLines = new ArrayList<>();  // frontmatter 内的行（不含 ---）
        List<String> bodyLines = new ArrayList<>();  // 正文行
        boolean hasFrontmatter = false;
        int closeIdx = -1;

        // 检测 frontmatter 并分离
        if (lines.length > 0 && lines[0].trim().equals("---")) {
            for (int i = 1; i < lines.length; i++) {
                if (lines[i].trim().equals("---")) {
                    hasFrontmatter = true;
                    closeIdx = i;
                    break;
                }
                metaLines.add(lines[i]);
            }
        }

        if (hasFrontmatter) {
            // 从关闭 --- 之后提取正文（跳过空行）
            int bodyStart = closeIdx + 1;
            while (bodyStart < lines.length && lines[bodyStart].trim().isEmpty()) {
                bodyStart++;
            }
            for (int i = bodyStart; i < lines.length; i++) {
                bodyLines.add(lines[i]);
            }
        } else {
            // 无 frontmatter：提取元数据行和正文
            String[] prefixes = {"name:", "description:", "inputs:", "outputs:"};
            int metaEnd = -1;
            for (int i = 0; i < lines.length && i < 30; i++) {
                String trimmed = lines[i].trim();
                if (trimmed.isEmpty() || trimmed.startsWith("<!--") || trimmed.startsWith("#")) {
                    if (metaEnd >= 0) break;
                    continue;
                }
                String lower = trimmed.toLowerCase();
                boolean isMetadata = false;
                for (String prefix : prefixes) {
                    if (lower.startsWith(prefix)) {
                        isMetadata = true;
                        break;
                    }
                }
                if (isMetadata) {
                    metaLines.add(trimmed);
                    metaEnd = i;
                } else if (metaEnd >= 0) {
                    // 元数据之后遇到了非元数据行
                    break;
                }
            }

            // 提取正文
            if (metaEnd >= 0) {
                int bodyStart = metaEnd + 1;
                while (bodyStart < lines.length && lines[bodyStart].trim().isEmpty()) {
                    bodyStart++;
                }
                for (int i = bodyStart; i < lines.length; i++) {
                    bodyLines.add(lines[i]);
                }
            } else {
                // 完全没有元数据，全部当作正文
                for (String line : lines) {
                    bodyLines.add(line);
                }
            }
        }

        // 合并 metaLines：替换标准字段，保留其他字段
        ObjectMapper mapper = new ObjectMapper();
        Set<String> standardKeys = Set.of("name", "displayname", "description", "cangenerateskill", "inputs", "outputs");
        List<String> preserved = new ArrayList<>();  // 非标准字段
        for (String line : metaLines) {
            String lower = line.toLowerCase();
            boolean isStandard = false;
            for (String key : standardKeys) {
                if (lower.startsWith(key + ":")) {
                    isStandard = true;
                    break;
                }
            }
            if (!isStandard) {
                preserved.add(line);
            }
        }

        // 重建 frontmatter：标准字段按顺序 + 非标准字段
        List<String> newMeta = new ArrayList<>();
        newMeta.add("name: " + name);
        if (displayName != null && !displayName.isEmpty()) {
            newMeta.add("displayName: " + displayName);
        }
        newMeta.add("description: " + description);
        newMeta.add("canGenerateSkill: " + canGenerateSkill);
        try {
            if (inputs != null && !inputs.isEmpty()) {
                newMeta.add("inputs: " + mapper.writeValueAsString(inputs));
            }
            if (outputs != null && !outputs.isEmpty()) {
                newMeta.add("outputs: " + mapper.writeValueAsString(outputs));
            }
        } catch (Exception e) {
            log.warn("序列化 inputs/outputs 失败", e);
        }
        newMeta.addAll(preserved);

        // 用 body 替换正文（如果 body 非空则使用新 body，否则保留原正文）
        String bodyContent = (body != null && !body.isEmpty()) ? body : String.join("\n", bodyLines);

        return "---\n" + String.join("\n", newMeta) + "\n---\n\n" + bodyContent;
    }

    @Override
    public List<FileNodeDTO> listSkillFiles(String folderName) {
        Path skillDir = resolveSkillDir(folderName);
        validateSkillDir(skillDir, folderName);
        try {
            return buildFileTree(skillDir, skillDir);
        } catch (IOException e) {
            log.error("列出文件失败: {}", skillDir, e);
            throw new BusinessException("列出文件失败");
        }
    }

    @Override
    public String readFile(String folderName, String relativePath) {
        Path file = resolveSkillPath(folderName, relativePath);
        validateWithinSkillDir(file, folderName);
        if (!Files.exists(file) || Files.isDirectory(file)) {
            throw new BusinessException(404, "文件不存在: " + relativePath);
        }
        try {
            return Files.readString(file, StandardCharsets.UTF_8);
        } catch (IOException e) {
            log.error("读取文件失败: {}", file, e);
            throw new BusinessException("读取文件失败");
        }
    }

    @Override
    public void saveFile(String folderName, String relativePath, String content) {
        Path file = resolveSkillPath(folderName, relativePath);
        validateWithinSkillDir(file, folderName);
        if (Files.isDirectory(file)) {
            throw new BusinessException("目标是目录，不能保存: " + relativePath);
        }
        try {
            Files.createDirectories(file.getParent());
            Files.writeString(file, content, StandardCharsets.UTF_8);
            log.info("保存文件成功: {}", file);
            hermesSkillSyncService.syncAfterWrite();
        } catch (IOException e) {
            log.error("保存文件失败: {}", file, e);
            throw new BusinessException("保存文件失败");
        }
    }

    @Override
    public void createFileOrDir(String folderName, FileCreateDTO dto) {
        if (dto.getPath() == null || dto.getPath().trim().isEmpty()) {
            throw new BusinessException("路径不能为空");
        }
        Path target = resolveSkillPath(folderName, dto.getPath());
        validateWithinSkillDir(target, folderName);

        if (Files.exists(target)) {
            throw new BusinessException(409, "已存在: " + dto.getPath());
        }

        try {
            if ("directory".equals(dto.getType())) {
                Files.createDirectories(target);
                log.info("创建目录成功: {}", target);
            } else {
                Files.createDirectories(target.getParent());
                Files.writeString(target, "", StandardCharsets.UTF_8);
                log.info("创建文件成功: {}", target);
            }
            hermesSkillSyncService.syncAfterWrite();
        } catch (IOException e) {
            log.error("创建失败: {}", target, e);
            throw new BusinessException("创建失败");
        }
    }

    @Override
    public void deleteFile(String folderName, String relativePath) {
        if (relativePath == null || relativePath.trim().isEmpty()) {
            throw new BusinessException("路径不能为空");
        }
        // 禁止删除 SKILL.md
        if (SKILL_MD_FILENAME.equalsIgnoreCase(relativePath)) {
            throw new BusinessException("不能删除 SKILL.md 文件");
        }
        Path target = resolveSkillPath(folderName, relativePath);
        validateWithinSkillDir(target, folderName);

        if (!Files.exists(target)) {
            throw new BusinessException(404, "不存在: " + relativePath);
        }

        try {
            if (Files.isDirectory(target)) {
                FileUtils.deleteDirectory(target.toFile());
            } else {
                Files.delete(target);
            }
            log.info("删除成功: {}", target);
            hermesSkillSyncService.syncAfterWrite();
        } catch (IOException e) {
            log.error("删除失败: {}", target, e);
            throw new BusinessException("删除失败");
        }
    }

    // ==================== 私有方法 ====================

    private Path getBasePath() {
        Path basePath = Paths.get(skillProperties.getBasePath()).toAbsolutePath();
        if (!Files.exists(basePath)) {
            try {
                Files.createDirectories(basePath);
            } catch (IOException e) {
                throw new BusinessException("Skills 目录不存在且无法创建: " + basePath);
            }
        }
        return basePath;
    }

    /**
     * 解析 skill 目录绝对路径并校验不越界（防止 folderName 含 ../ 引发路径穿越）。
     * 不强制要求目录存在；调用方根据语义自行校验。
     */
    private Path resolveSkillDir(String folderName) {
        Path base = getBasePath().normalize();
        Path skillDir = base.resolve(folderName).normalize();
        if (!skillDir.startsWith(base)) {
            throw new BusinessException("非法 folderName: " + folderName);
        }
        return skillDir;
    }

    /**
     * 解压 zip 文件到目标目录
     */
    private void extractZip(Path zipPath, Path targetDir) throws IOException {
        Files.createDirectories(targetDir);
        try (ZipFile zipFile = new ZipFile(zipPath.toFile())) {
            Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
            while (entries.hasMoreElements()) {
                ZipArchiveEntry entry = entries.nextElement();
                Path entryPath = targetDir.resolve(entry.getName());

                // 防止路径穿越攻击
                if (!entryPath.normalize().startsWith(targetDir.normalize())) {
                    throw new BusinessException("压缩包包含非法路径: " + entry.getName());
                }

                if (entry.isDirectory()) {
                    Files.createDirectories(entryPath);
                } else {
                    Files.createDirectories(entryPath.getParent());
                    try (InputStream is = zipFile.getInputStream(entry)) {
                        Files.copy(is, entryPath, StandardCopyOption.REPLACE_EXISTING);
                    }
                }
            }
        }
    }

    /**
     * 查找解压目录中第一个包含 SKILL.md 的第一层子目录
     */
    private Path findFirstSkillDirectory(Path extractedDir) throws IOException {
        try (Stream<Path> paths = Files.list(extractedDir)) {
            // 先检查第一层子目录
            Optional<Path> first = paths.filter(Files::isDirectory)
                    .filter(dir -> Files.exists(dir.resolve(SKILL_MD_FILENAME)))
                    .findFirst();

            if (first.isPresent()) {
                return first.get();
            }
        }

        // 检查 extractedDir 自身（zip 内没有外层包装目录的情况）
        if (Files.exists(extractedDir.resolve(SKILL_MD_FILENAME))) {
            return extractedDir;
        }

        // 检查嵌套情况：zip 内只有一个文件夹，该文件夹内包含 SKILL.md
        try (Stream<Path> paths = Files.list(extractedDir)) {
            List<Path> allItems = paths.toList();
            if (allItems.size() == 1 && Files.isDirectory(allItems.get(0))) {
                Path nested = allItems.get(0);
                if (Files.exists(nested.resolve(SKILL_MD_FILENAME))) {
                    return nested;
                }
            }
        }

        return null;
    }

    /**
     * 从 SKILL.md 完整内容中提取正文部分（元数据之后的内容）
     */
    private String extractBody(String fullContent) {
        if (fullContent == null || fullContent.isEmpty()) return "";

        String[] lines = fullContent.split("\n");
        int bodyStart = -1;

        // 检测 --- frontmatter 格式
        if (lines.length > 0 && lines[0].trim().equals("---")) {
            int closeIdx = -1;
            for (int i = 1; i < lines.length; i++) {
                if (lines[i].trim().equals("---")) {
                    closeIdx = i;
                    break;
                }
            }
            if (closeIdx >= 0) {
                bodyStart = closeIdx + 1;
            }
        }

        // 回退：无 frontmatter，按元数据行前缀检测
        if (bodyStart < 0) {
            int metadataFieldsFound = 0;
            String[] prefixes = {"name:", "description:", "inputs:", "outputs:"};

            for (int i = 0; i < lines.length && i < 30; i++) {
                String trimmed = lines[i].trim();
                if (trimmed.isEmpty() || trimmed.startsWith("<!--") || trimmed.startsWith("#") || trimmed.equals("---")) {
                    continue;
                }

                String lower = trimmed.toLowerCase();
                boolean isMetadata = false;
                for (String prefix : prefixes) {
                    if (lower.startsWith(prefix)) {
                        metadataFieldsFound++;
                        isMetadata = true;
                        break;
                    }
                }

                if (isMetadata) {
                    if (metadataFieldsFound == 4) {
                        bodyStart = i + 1;
                        break;
                    }
                    continue;
                }

                bodyStart = i;
                break;
            }
        }

        if (bodyStart < 0 || bodyStart >= lines.length) return "";

        // 跳过正文开头的空行
        while (bodyStart < lines.length && lines[bodyStart].trim().isEmpty()) {
            bodyStart++;
        }
        if (bodyStart >= lines.length) return "";

        return String.join("\n", Arrays.copyOfRange(lines, bodyStart, lines.length));
    }

    /**
     * 递归构建文件树
     */
    private List<FileNodeDTO> buildFileTree(Path dir, Path root) throws IOException {
        try (Stream<Path> paths = Files.list(dir)) {
            return paths
                    .filter(p -> {
                        String name = p.getFileName().toString();
                        return !name.equals(VERSIONS_DIR) && !name.equals(VERSION_META_FILE);
                    })
                    .sorted(Comparator.comparing((Path p) -> !Files.isDirectory(p))
                            .thenComparing(p -> p.getFileName().toString()))
                    .map(p -> {
                        FileNodeDTO node = new FileNodeDTO();
                        node.setName(p.getFileName().toString());
                        node.setPath(root.relativize(p).toString().replace('\\', '/'));
                        if (Files.isDirectory(p)) {
                            node.setType("directory");
                            try {
                                node.setChildren(buildFileTree(p, root));
                            } catch (IOException e) {
                                node.setChildren(Collections.emptyList());
                            }
                        } else {
                            node.setType("file");
                        }
                        return node;
                    })
                    .collect(Collectors.toList());
        }
    }

    /**
     * 解析 Skill 目录下的相对路径
     */
    private Path resolveSkillPath(String folderName, String relativePath) {
        Path skillDir = resolveSkillDir(folderName);
        validateSkillDir(skillDir, folderName);
        Path resolved = skillDir.resolve(relativePath).normalize();
        // 路径穿越检查
        if (!resolved.startsWith(skillDir.normalize())) {
            throw new BusinessException("非法路径: " + relativePath);
        }
        return resolved;
    }

    /**
     * 验证 Skill 目录存在
     */
    private void validateSkillDir(Path skillDir, String folderName) {
        if (!Files.exists(skillDir) || !Files.isDirectory(skillDir)) {
            throw new BusinessException(404, "Skill 不存在: " + folderName);
        }
    }

    /**
     * 验证路径在 Skill 目录内（路径穿越检查）
     */
    private void validateWithinSkillDir(Path path, String folderName) {
        Path skillDir = resolveSkillDir(folderName).normalize();
        if (!path.normalize().startsWith(skillDir)) {
            throw new BusinessException("非法路径");
        }
    }
}
