package com.agent.management.engine;

import com.agent.management.config.SkillProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

/**
 * 将 Skills 源目录精确镜像同步到固定 HERMES_HOME 的 skills 子目录，
 * 供 Hermes Bridge Python AIAgent 在执行时按 HERMES_HOME 读取 Skill。
 *
 * <p>镜像语义：
 * <ul>
 *   <li>目标侧存在而源侧不存在的 skill 目录会被删除（处理 skill 删除/重命名）；</li>
 *   <li>每个 skill 目录先整体删除再重新复制（处理 skill 内文件删除）；</li>
 *   <li>{@code _versions} 历史版本子目录不参与复制（避免 hermes-agent 递归扫描
 *       把版本快照下的 SKILL.md 识别为候选，触发 collision detection）。</li>
 *   <li>每个 skill 下<strong>直接包含 SKILL.md 的子目录</strong>不参与复制（任意深度），
 *       防止技能内嵌套 SKILL.md 触发 hermes-agent 的 {@code Ambiguous skill name} 冲突。
 *       常见于 AI 技能生成场景的残留产物（如 {@code generated_skills/}、
 *       {@code tmp_smoke_generated/}）。判定方式：{@code preVisitDirectory} 在每个非根
 *       子目录处检查自身是否直接含 SKILL.md，命中则 {@code SKIP_SUBTREE}。
 *       <strong>注意：技能根目录自身的 SKILL.md 是合法主入口，不受此规则影响。</strong></li>
 * </ul>
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class SkillFileSyncer {

    /** 单次同步允许的最大 skill 目录数量 */
    private static final int MAX_SKILL_COUNT = 200;
    /** 单次同步允许的最大总字节数（500MB），超过则中止并抛异常 */
    private static final long MAX_TOTAL_BYTES = 500L * 1024 * 1024;
    /** 合法的技能索引文件名，技能根目录下必须存在此文件 */
    private static final String SKILL_MD_FILENAME = "SKILL.md";

    private final SkillProperties skillProperties;

    /**
     * 将源 skills 根目录下所有 skill 子目录镜像复制到目标目录。
     *
     * @param targetSkillsDir 目标 skills 目录（固定 HERMES_HOME 下的 skills 子目录）
     * @return 实际复制的 skill 文件夹数量
     * @throws IOException 复制失败或超过上限时抛出
     */
    public int syncTo(Path targetSkillsDir) throws IOException {
        Path source = Paths.get(skillProperties.getBasePath()).toAbsolutePath().normalize();
        if (!Files.isDirectory(source)) {
            log.warn("[SkillFileSyncer] Skills 源目录不存在，跳过同步: {}", source);
            return 0;
        }
        Files.createDirectories(targetSkillsDir);

        // 1. 收集源侧 skill 目录名，清除目标侧残留（skill 被删除/重命名后目标侧的旧副本）
        Set<String> sourceNames = new HashSet<>();
        try (Stream<Path> entries = Files.list(source)) {
            entries.filter(Files::isDirectory)
                    .forEach(p -> sourceNames.add(p.getFileName().toString()));
        }
        try (Stream<Path> entries = Files.list(targetSkillsDir)) {
            for (Path entry : (Iterable<Path>) entries::iterator) {
                if (!Files.isDirectory(entry)) continue;
                String name = entry.getFileName().toString();
                if (!sourceNames.contains(name)) {
                    deleteRecursively(entry);
                    log.info("[SkillFileSyncer] 清除目标侧残留 skill: {}", name);
                }
            }
        }

        // 2. 逐 skill 整目录先删后拷，保证文件级精确镜像（skill 内被删文件不残留）
        long[] totalBytes = {0L};
        int copied = 0;
        try (Stream<Path> entries = Files.list(source)) {
            for (Path entry : (Iterable<Path>) entries::iterator) {
                if (!Files.isDirectory(entry)) continue;
                if (copied >= MAX_SKILL_COUNT) {
                    log.warn("[SkillFileSyncer] 已达 skill 数量上限 {}，中止后续同步", MAX_SKILL_COUNT);
                    break;
                }
                Path target = targetSkillsDir.resolve(entry.getFileName());
                if (Files.exists(target)) {
                    deleteRecursively(target);
                }
                long[] dirBytes = {0L};
                copyDirectory(entry, target, dirBytes, MAX_TOTAL_BYTES - totalBytes[0]);
                totalBytes[0] += dirBytes[0];
                copied++;
            }
        }
        log.info("[SkillFileSyncer] 同步 {} 个 skill, 总计 {} 字节 -> {}", copied, totalBytes[0], targetSkillsDir);
        return copied;
    }

    private void deleteRecursively(Path dir) throws IOException {
        try (Stream<Path> walk = Files.walk(dir)) {
            List<Path> paths = walk.sorted(Comparator.reverseOrder()).toList();
            for (Path p : paths) {
                Files.delete(p);
            }
        }
    }

    /**
     * 复制单个 skill 目录，同时累计已复制字节数，超过剩余配额则抛 IOException。
     *
     * <p>跳过规则（在 {@code preVisitDirectory} 中按目录层级逐层判定）：
     * <ul>
     *   <li>{@code _versions} 目录：整体跳过，避免历史版本快照的 SKILL.md 被识别为候选。</li>
     *   <li>非根子目录直接含 {@code SKILL.md}：整体跳过，避免技能内嵌套 SKILL.md
     *       触发 hermes-agent 的 {@code Ambiguous skill name} 冲突。
     *       常见于 AI 技能生成的残留产物（{@code generated_skills/}、{@code tmp_smoke_generated/}）。</li>
     * </ul>
     * <p>注意：技能根目录（{@code src} 自身）含 SKILL.md 是合法主入口，不命中跳过条件。
     */
    private void copyDirectory(Path src, Path dst, long[] dirBytes, long remainingQuota) throws IOException {
        Files.walkFileTree(src, new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                String dirName = dir.getFileName() != null ? dir.getFileName().toString() : "";
                // 跳过 _versions 子目录：避免 hermes-agent 的 iter_skill_index_files 递归扫描时
                // 把版本快照下的 SKILL.md 也识别为候选，触发 collision detection
                // ("Ambiguous skill name") 导致 skill_view 拒绝加载。
                if ("_versions".equals(dirName)) {
                    return FileVisitResult.SKIP_SUBTREE;
                }
                // 跳过非根子目录中直接含 SKILL.md 的目录：技能内只允许根目录有一个 SKILL.md
                // 主入口，子目录中出现 SKILL.md 一律视为嵌套技能残留（常见于 AI 生成场景的
                // generated_skills/、tmp_smoke_generated/ 等），整树跳过避免 Hermes 扫描冲突。
                // 注意：dir == src 的情况（技能根目录自身）必须放行，否则会跳过整个技能。
                if (!dir.equals(src) && Files.exists(dir.resolve(SKILL_MD_FILENAME))) {
                    log.warn("[SkillFileSyncer] 跳过 skill {} 下含 {} 的子目录 {}（防止 Hermes 扫描冲突）",
                            src.getFileName(), SKILL_MD_FILENAME, dirName);
                    return FileVisitResult.SKIP_SUBTREE;
                }
                Files.createDirectories(dst.resolve(src.relativize(dir)));
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                long size = attrs.size();
                if (dirBytes[0] + size > remainingQuota) {
                    throw new IOException("复制超出剩余配额 " + remainingQuota + " 字节，可能存在异常大文件");
                }
                Files.copy(file, dst.resolve(src.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
                dirBytes[0] += size;
                return FileVisitResult.CONTINUE;
            }
        });
    }
}
