package com.gzzm.lobster.skill;

import com.gzzm.lobster.common.SkillRuntimeKind;
import com.gzzm.lobster.common.SkillScope;
import com.gzzm.lobster.config.LobsterConfig;
import com.gzzm.platform.commons.Tools;
import net.cyan.arachne.annotation.Service;
import net.cyan.nest.annotation.Inject;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * SystemSkillLoader —— 启动时扫描文件系统、把系统 skill 登记为 {@link SkillDefinition}.
 *
 * <p>两条根路径分别承载两类系统 skill：
 * <ul>
 *   <li>{@link LobsterConfig#getSystemSandboxSkillDir()}（默认 {@code deploy/skills}）——
 *     子目录含 {@code SKILL.md}，可选带 {@code scripts/}、字体、模板等资源目录，
 *     作为<b>沙箱资源类</b> skill 注册；
 *     {@code assetBundleRef} 设为 {@code fs://<absolute-path>}，{@link SkillAssetService}
 *     按该协议直接绑定目录，不做解压。</li>
 *   <li>{@link LobsterConfig#getSystemGuidanceSkillDir()}（默认 {@code WEB-INF/skills}）——
 *     子目录只含 {@code SKILL.md}，作为<b>guidance 类</b> skill 注册；SKILL.md 正文直接写入
 *     {@code SkillDefinition.guidance} 字段，不走沙箱。</li>
 * </ul>
 *
 * <p>ID 规则：{@code sys_<name>}（name 来自 frontmatter 或目录名）；同名 skill 以文件为准，
 * 启动时 upsert——管理员经 Admin UI 调整过的 enabled / orgId / 附加元信息<b>不会</b>被覆盖。
 *
 * <p>路径解析：相对路径按 {@code catalina.base}（找不到则退回 {@code user.dir}）解析绝对路径；
 * 绝对路径按原样使用。找不到目录只告警，不抛——允许空部署.
 */
@Service
public class SystemSkillLoader {

    @Inject private SkillDefinitionDao skillDao;

    /** thunwind DAO 跨线程保护 */
    private SkillDefinitionDao skillDao() {
        try {
            SkillDefinitionDao d = Tools.getBean(SkillDefinitionDao.class);
            if (d != null) return d;
        } catch (Throwable ignore) { /* fallback */ }
        return skillDao;
    }

    /** 扫描两条根路径并 upsert；bootstrap 启动时调. */
    public LoadResult loadAll() {
        return loadAll(null);
    }

    /**
     * 带 webapp 根的扫描 —— 生产部署里 {@code WEB-INF/skills} 只能通过
     * {@code ServletContext.getRealPath("/")} 定位；BootstrapServlet 会传进来.
     */
    public LoadResult loadAll(String webappRoot) {
        LoadResult r = new LoadResult();
        Set<String> discovered = new HashSet<>();
        Path sandboxRoot = resolveRoot(LobsterConfig.getSystemSandboxSkillDir(), webappRoot);
        Path guidanceRoot = resolveRoot(LobsterConfig.getSystemGuidanceSkillDir(), webappRoot);
        boolean scannedAllRoots = true;
        try {
            if (sandboxRoot != null && Files.isDirectory(sandboxRoot)) {
                scanDir(sandboxRoot, true, r, discovered);
            } else {
                scannedAllRoots = false;
                Tools.log("[SystemSkillLoader] sandbox skill dir not found: "
                        + LobsterConfig.getSystemSandboxSkillDir()
                        + " (resolved=" + sandboxRoot + ")");
            }
            if (guidanceRoot != null && Files.isDirectory(guidanceRoot)) {
                scanDir(guidanceRoot, false, r, discovered);
            } else {
                scannedAllRoots = false;
                Tools.log("[SystemSkillLoader] guidance skill dir not found: "
                        + LobsterConfig.getSystemGuidanceSkillDir()
                        + " (resolved=" + guidanceRoot + ")");
            }
            if (scannedAllRoots) {
                reconcileMissingSystemSkills(discovered, r);
            } else {
                Tools.log("[SystemSkillLoader] skip stale-system-skill reconciliation because one or more roots were not scanned");
            }
        } catch (Throwable t) {
            try { Tools.log("[SystemSkillLoader] scan failed", t); } catch (Throwable ignore) { /* ignore */ }
        }
        Tools.log("[SystemSkillLoader] done — inserted=" + r.inserted
                + ", updated=" + r.updated
                + ", reEnabled=" + r.reEnabled
                + ", disabledMissing=" + r.disabledMissing
                + ", skipped=" + r.skipped);
        return r;
    }

    private void scanDir(Path root, boolean sandbox, LoadResult r, Set<String> discovered) throws IOException {
        try (java.util.stream.Stream<Path> s = Files.list(root)) {
            s.filter(Files::isDirectory).forEach(child -> {
                try {
                    loadOne(child, sandbox, r, discovered);
                } catch (Throwable t) {
                    r.skipped++;
                    try { Tools.log("[SystemSkillLoader] load failed: " + child, t); }
                    catch (Throwable ignore) { /* ignore */ }
                }
            });
        }
    }

    private void loadOne(Path dir, boolean sandbox, LoadResult r, Set<String> discovered) throws Exception {
        Path skillMd = dir.resolve("SKILL.md");
        if (!Files.exists(skillMd)) { r.skipped++; return; }
        String body = new String(Files.readAllBytes(skillMd), StandardCharsets.UTF_8);
        Frontmatter fm = parseFrontmatter(body);
        String name = (fm.name != null && !fm.name.isEmpty()) ? fm.name : dir.getFileName().toString();
        // 合并 description 与 when_to_use 作为薄索引（triggerCondition）；两者都有时拼接.
        String desc;
        if (fm.description != null && !fm.description.isEmpty()
                && fm.whenToUse != null && !fm.whenToUse.isEmpty()) {
            desc = fm.description + " | when_to_use: " + fm.whenToUse;
        } else if (fm.description != null && !fm.description.isEmpty()) {
            desc = fm.description;
        } else {
            desc = fm.whenToUse;
        }
        // deploy/skills 是 asset-bearing 命名空间：scripts/ 是可选执行入口，字体/模板等资源目录
        // 也必须作为 bundle 暴露给 read_skill_resource 与 code_exec activated_skill。
        SkillRuntimeKind kind = sandbox ? SkillRuntimeKind.iterative : SkillRuntimeKind.single_shot;

        String skillId = "sys_" + sanitizeId(name);
        discovered.add(skillId);
        SkillDefinition existing = skillDao().getSkill(skillId);
        SkillDefinition s = existing == null ? new SkillDefinition() : existing;
        boolean isNew = existing == null;
        if (isNew) {
            s.setSkillId(skillId);
            s.setEnabled(Boolean.TRUE);
            s.setVersion(1);
            s.setCreateTime(new Date());
        } else if (contentChanged(existing, body)) {
            // 只在 SKILL.md 正文真的变了才 bump version；避免每次重启都涨
            s.setVersion((s.getVersion() == null ? 1 : s.getVersion()) + 1);
        }
        if (!isNew && !Boolean.TRUE.equals(s.getEnabled())) {
            s.setEnabled(Boolean.TRUE);
            r.reEnabled++;
        }
        s.setName(name);
        s.setScope(SkillScope.system);
        s.setTriggerCondition(truncate(desc, 500));
        s.setGuidance(body);
        s.setRuntimeKind(kind);
        s.setUpdateTime(new Date());

        s.setAssetBundleRef(assetBundleRefFor(dir, sandbox));
        skillDao().save(s);
        if (isNew) r.inserted++; else r.updated++;
    }

    private void reconcileMissingSystemSkills(Set<String> discovered, LoadResult r) throws Exception {
        List<SkillDefinition> existing = skillDao().listAllByScope(SkillScope.system);
        for (SkillDefinition s : existing) {
            String skillId = s.getSkillId();
            if (skillId == null || discovered.contains(skillId)) continue;
            if (!skillId.startsWith("sys_")) continue;
            if (!Boolean.TRUE.equals(s.getEnabled())) continue;
            s.setEnabled(Boolean.FALSE);
            s.setUpdateTime(new Date());
            skillDao().save(s);
            r.disabledMissing++;
            try { Tools.log("[SystemSkillLoader] disabled missing system skill: " + skillId); }
            catch (Throwable ignore) { /* ignore */ }
        }
    }

    static String assetBundleRefFor(Path dir, boolean sandbox) {
        if (!sandbox) return null;
        // fs://<absolute> —— SkillAssetService.ensureExtracted 按此前缀绑定目录
        return "fs://" + dir.toAbsolutePath().toString().replace('\\', '/');
    }

    /**
     * 解析候选路径，按优先级依次尝试：
     * <ol>
     *   <li>绝对路径原样</li>
     *   <li>webappRoot（由 BootstrapServlet 传入的 {@code getServletContext().getRealPath("/")}）
     *       —— {@code WEB-INF/skills} 走这里</li>
     *   <li>{@code catalina.base}（Tomcat 根）—— 生产部署时 {@code deploy/skills} 常指向
     *       Tomcat 同级或子级目录</li>
     *   <li>{@code user.dir}（开发态 / 测试）</li>
     * </ol>
     * 返回第一个 {@code isDirectory()} 为真的候选；全不中返回最优先的非 null 候选（供 caller 告警）.
     */
    private Path resolveRoot(String raw, String webappRoot) {
        if (raw == null || raw.isEmpty()) return null;
        Path p = Paths.get(raw);
        if (p.isAbsolute()) return p;
        List<Path> candidates = new ArrayList<>();
        if (webappRoot != null && !webappRoot.isEmpty()) {
            candidates.add(Paths.get(webappRoot).resolve(raw).toAbsolutePath().normalize());
        }
        String base = System.getProperty("catalina.base");
        if (base != null && !base.isEmpty()) {
            candidates.add(Paths.get(base).resolve(raw).toAbsolutePath().normalize());
        }
        String ud = System.getProperty("user.dir", ".");
        candidates.add(Paths.get(ud).resolve(raw).toAbsolutePath().normalize());
        for (Path c : candidates) {
            if (Files.isDirectory(c)) return c;
        }
        // 都不存在：返回第一个候选，让 loadAll 的日志打出来
        return candidates.isEmpty() ? null : candidates.get(0);
    }

    /** SKILL.md 内容 hash 判断是否真的变了（避免每次重启 bump version）. */
    private static boolean contentChanged(SkillDefinition existing, String newBody) {
        if (existing == null) return true;
        String old = existing.getGuidance();
        if (old == null) return newBody != null;
        if (newBody == null) return true;
        return !old.equals(newBody);
    }

    /** 非常精简的 frontmatter 解析：只关心 name / description（LICENSE 等忽略）. */
    static Frontmatter parseFrontmatter(String body) {
        Frontmatter fm = new Frontmatter();
        if (body == null || body.isEmpty()) return fm;
        String[] lines = body.split("\\r?\\n", -1);
        if (lines.length < 2 || !"---".equals(lines[0].trim())) return fm;
        int end = -1;
        for (int i = 1; i < lines.length; i++) {
            if ("---".equals(lines[i].trim())) { end = i; break; }
        }
        if (end < 0) return fm;
        StringBuilder acc = new StringBuilder();
        String currentKey = null;
        for (int i = 1; i < end; i++) {
            String line = lines[i];
            // 简化：按 `key: value` 切，value 跨行用前导空格延续
            if (!line.isEmpty() && Character.isWhitespace(line.charAt(0)) && currentKey != null) {
                acc.append(' ').append(line.trim());
                continue;
            }
            if (currentKey != null) applyKv(fm, currentKey, acc.toString().trim());
            int colon = line.indexOf(':');
            if (colon <= 0) { currentKey = null; acc.setLength(0); continue; }
            currentKey = line.substring(0, colon).trim();
            String v = line.substring(colon + 1).trim();
            // 去掉配对的引号
            if ((v.startsWith("\"") && v.endsWith("\"") && v.length() >= 2)
                    || (v.startsWith("'") && v.endsWith("'") && v.length() >= 2)) {
                v = v.substring(1, v.length() - 1);
            }
            acc.setLength(0);
            acc.append(v);
        }
        if (currentKey != null) applyKv(fm, currentKey, acc.toString().trim());
        return fm;
    }

    private static void applyKv(Frontmatter fm, String k, String v) {
        if ("name".equalsIgnoreCase(k)) fm.name = v;
        else if ("description".equalsIgnoreCase(k)) fm.description = v;
        // 对齐 Claude Code：when_to_use 是 skill 触发条件的另一种表达，合并进 description
        // 供 listThinIndex 里的 triggerCondition 索引，提高 LLM 命中率.
        else if ("when_to_use".equalsIgnoreCase(k) || "when-to-use".equalsIgnoreCase(k)) {
            fm.whenToUse = v;
        }
    }

    private static String sanitizeId(String s) {
        if (s == null) return "unnamed";
        StringBuilder sb = new StringBuilder(s.length());
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (Character.isLetterOrDigit(c) || c == '-' || c == '_') sb.append(c);
            else sb.append('_');
        }
        String out = sb.toString();
        if (out.length() > 50) out = out.substring(0, 50);
        return out.isEmpty() ? "unnamed" : out;
    }

    private static String truncate(String s, int max) {
        if (s == null) return null;
        return s.length() <= max ? s : s.substring(0, max);
    }

    public static final class Frontmatter {
        public String name;
        public String description;
        /** Claude Code 兼容字段：何时触发本 skill；与 description 合并进 triggerCondition. */
        public String whenToUse;
    }

    public static final class LoadResult {
        public int inserted;
        public int updated;
        public int reEnabled;
        public int disabledMissing;
        public int skipped;
        public Map<String, Object> toMap() {
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("inserted", inserted);
            m.put("updated", updated);
            m.put("reEnabled", reEnabled);
            m.put("disabledMissing", disabledMissing);
            m.put("skipped", skipped);
            return m;
        }
    }
}
