package com.gzzm.lobster.tool.builtin;

import com.gzzm.lobster.common.SkillScope;
import com.gzzm.lobster.common.ToolCategory;
import com.gzzm.lobster.common.ToolRiskLevel;
import com.gzzm.lobster.skill.SkillAssetService;
import com.gzzm.lobster.skill.SkillDefinition;
import com.gzzm.lobster.skill.SkillInvocationDao;
import com.gzzm.lobster.skill.SkillInvocationLog;
import com.gzzm.lobster.skill.SkillService;
import com.gzzm.lobster.tool.*;
import com.gzzm.platform.commons.Tools;
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.util.*;
import java.util.stream.Stream;

/**
 * SkillTools —— Layer 3 Skill 调度工具 / list_skills + use_skill.
 */
public class SkillTools {

    @Inject private SkillService skillService;
    @Inject private SkillAssetService skillAssetService;
    @Inject private SkillInvocationDao skillInvocationDao;

    public void registerTo(ToolRegistry registry) {
        registry.register(listDef(), this::list);
        registry.register(useDef(), this::use);
        registry.register(readResourceDef(), this::readResource);
    }

    private BuiltinToolDefinition listDef() {
        return BuiltinToolDefinition.builder()
                .name("list_skills")
                .displayName("列出可用 Skill")
                .description("列出当前可用的 Skill，返回 id/name/description。"
                        + "description 说明'什么时候该用这个 skill'；命中后调用 use_skill 读取完整方法论。")
                .category(ToolCategory.SKILL)
                .risk(ToolRiskLevel.READ_ONLY)
                .inputSchema(SchemaBuilder.obj()
                        .propEnum("scope", "范围筛选", "system", "org", "ALL")
                        .build())
                .build();
    }

    private ToolResult list(ToolContext ctx, Map<String, Object> args) throws Exception {
        String scopeStr = asStr(args.get("scope"));
        List<SkillDefinition> defs;
        if (scopeStr == null || scopeStr.isEmpty() || "ALL".equalsIgnoreCase(scopeStr)) {
            defs = skillService.listAll();
        } else {
            try { defs = skillService.listByScope(SkillScope.valueOf(scopeStr)); }
            catch (Exception e) { defs = skillService.listAll(); }
        }
        List<Map<String, Object>> out = new ArrayList<>();
        for (SkillDefinition d : defs) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("skillId", d.getSkillId());
            row.put("name", d.getName());
            row.put("scope", d.getScope() == null ? "system" : d.getScope().name());
            // description 出口字段：对齐 Claude Code 的 description 语义（"什么时候用"）。
            // DB 字段仍叫 triggerCondition（不做 rename 迁移，只在 API 层重命名输出 key）。
            row.put("description", d.getTriggerCondition());
            row.put("runtimeKind", d.getRuntimeKind() == null ? "single_shot" : d.getRuntimeKind().name());
            out.add(row);
        }
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("skills", out);
        return ToolResult.okData(data);
    }

    private BuiltinToolDefinition useDef() {
        return BuiltinToolDefinition.builder()
                .name("use_skill")
                .displayName("读取 Skill 方法论")
                .description("读取指定 skill 的完整 guidance（方法论）；调用结果即是该 skill "
                        + "的完整 SKILL.md 文档，后续按其中的步骤执行任务。"
                        + "需要重新查看方法论时继续调用本工具，不要使用其它工具拼接 skillId 或工具名。"
                        + "如果 SKILL.md 明确引用相对 Markdown 子文档（如 pptxgenjs.md），"
                        + "需要详细内容时调用 read_skill_resource(skillId, path) 按需读取。")
                .category(ToolCategory.SKILL)
                .risk(ToolRiskLevel.READ_ONLY)
                .inputSchema(SchemaBuilder.obj()
                        .prop("skillId", "string", "Skill ID")
                        .required("skillId")
                        .build())
                .build();
    }

    private ToolResult use(ToolContext ctx, Map<String, Object> args) throws Exception {
        String skillId = asStr(args.get("skillId"));
        SkillDefinition def = skillService.get(skillId);
        if (def == null) return ToolResult.error("skill not found: " + skillId);

        // 记录重复调用，但不再因为 duplicate 跳过 guidance 全文：历史 tool result 可能
        // 已被压缩、折叠或不在当前 send-view，use_skill 自身必须保持“读取完整方法论”的语义。
        // 激活 key 仅含 skillId，<b>不含 version</b>：对话进行中 admin 改了 guidance
        // 也不会影响已激活标记；新 thread 自然会看到新版本。
        boolean duplicate = skillService.activatedForThread(ctx.getThreadId()).contains(skillId);
        skillService.activateForThread(ctx.getThreadId(), skillId);
        recordInvocation(ctx, skillId, duplicate);

        Map<String, Object> data = new LinkedHashMap<>();
        data.put("skillId", skillId);
        data.put("name", def.getName());
        data.put("duplicate", duplicate);

        String guidance = def.getGuidance();
        if (guidance == null || guidance.isEmpty()) {
            guidance = "（该 skill 没有 guidance 正文）";
        }

        // D: bundle 文件清单——给 SKILL.md 没显式列资源时的兜底。
        // 作者已经在 guidance 里提到的文件路径会跳过，避免重复。
        String resourceAppendix = tryBuildResourceAppendix(skillId, guidance);
        if (resourceAppendix != null && !resourceAppendix.isEmpty()) {
            guidance = guidance + "\n\n" + resourceAppendix;
        }

        // guidance 全文通过 tool message 返回——这是 LLM 最自然读取的字段（也对齐 use_skill 工具
        // description 承诺的"结果即是 SKILL.md"）. 弱模型（如 deepseek-chat）若 guidance 放 data 里，
        // 有概率不看、反而去编造 artifactId 调 read_file 去找——上次 trace 实测踩过.
        // 前端在 lobsterStore.shortenUseSkillSummary 里把 frontmatter 剥掉再显示，tool-card chip 保持干净.
        // data 只保留 metadata，不重复塞 guidance（否则 LLM 会在 tool_result JSON 里读到两份一样的内容）.
        // 即使同一 thread 内重复调用，也重新返回全文：历史 tool result 可能已被压缩、折叠或不在当前
        // send-view 中；use_skill 的语义必须稳定为"读取完整方法论"，避免模型转而编造外置内容 ref.
        return ToolResult.ok(guidance, data);
    }

    private BuiltinToolDefinition readResourceDef() {
        return BuiltinToolDefinition.builder()
                .name("read_skill_resource")
                .displayName("读取 Skill 文本资源")
                .description("按需读取已激活 skill bundle 内的相对文本资源。"
                        + "仅当 use_skill 返回的 SKILL.md 明确引用了相对链接（如 pptxgenjs.md、"
                        + "editing.md、scripts/office/soffice.py），且需要查看该文档或脚本源码时使用。"
                        + "调用前必须已经 use_skill(skillId)。path 必须是 bundle 内相对路径，"
                        + "不要传 /skill/... 绝对路径、不要传外置 ref、不要用来读取用户文件或 artifact。"
                        + "若要执行脚本，不用本工具，改用 code_exec 并传容器内绝对路径 /skill/<skillId>/...")
                .category(ToolCategory.SKILL)
                .risk(ToolRiskLevel.READ_ONLY)
                .inputSchema(SchemaBuilder.obj()
                        .prop("skillId", "string", "已通过 use_skill 激活的 Skill ID")
                        .prop("path", "string", "Skill bundle 内相对文本资源路径，如 pptxgenjs.md 或 scripts/office/soffice.py")
                        .propInt("offset", "起始字符位置；缺省 0")
                        .propInt("limit", "本次读取的最大字符数；缺省 12000，硬上限 16000")
                        .required("skillId", "path")
                        .build())
                .build();
    }

    private ToolResult readResource(ToolContext ctx, Map<String, Object> args) throws Exception {
        String skillId = asStr(args.get("skillId"));
        String relPath = cleanSkillResourcePath(asStr(args.get("path")));
        if (skillId == null || skillId.isEmpty()) {
            return ToolResult.error("read_skill_resource.invalid: missing skillId");
        }
        if (relPath == null || relPath.isEmpty()) {
            return ToolResult.error("read_skill_resource.invalid: path must be a safe relative text resource path");
        }
        if (!skillService.activatedForThread(ctx.getThreadId()).contains(skillId)) {
            return ToolResult.error("read_skill_resource.not_activated: call use_skill(skillId) first");
        }

        Path bundleRoot;
        try {
            bundleRoot = skillAssetService.ensureExtracted(skillId);
        } catch (Throwable t) {
            return ToolResult.error("read_skill_resource.bundle_missing: " + safeMsg(t));
        }
        if (bundleRoot == null || !Files.isDirectory(bundleRoot)) {
            return ToolResult.error("read_skill_resource.bundle_missing: no asset bundle for " + skillId);
        }

        Path target = bundleRoot.resolve(relPath).normalize();
        if (!target.startsWith(bundleRoot) || !Files.isRegularFile(target)) {
            return ToolResult.error("read_skill_resource.not_found: " + relPath);
        }
        long bytes = Files.size(target);
        if (bytes > SKILL_RESOURCE_MAX_BYTES) {
            return ToolResult.error("read_skill_resource.too_large: " + relPath
                    + " exceeds " + SKILL_RESOURCE_MAX_BYTES + " bytes");
        }

        String body = new String(Files.readAllBytes(target), StandardCharsets.UTF_8);
        int offset = Math.max(0, asInt(args.get("offset"), 0));
        int limit = asInt(args.get("limit"), DEFAULT_SKILL_RESOURCE_LIMIT);
        if (limit <= 0) limit = DEFAULT_SKILL_RESOURCE_LIMIT;
        if (limit > MAX_SKILL_RESOURCE_LIMIT) limit = MAX_SKILL_RESOURCE_LIMIT;
        int start = Math.min(offset, body.length());
        int end = Math.min(body.length(), start + limit);
        String slice = body.substring(start, end);

        Map<String, Object> data = new LinkedHashMap<>();
        data.put("skillId", skillId);
        data.put("path", relPath);
        data.put("offset", offset);
        data.put("limit", limit);
        data.put("returnedChars", slice.length());
        data.put("totalChars", body.length());
        data.put("truncated", end < body.length());
        return ToolResult.ok(slice, data);
    }

    /** 扫描 skill bundle，列出 guidance 未提及的辅助文件——给模型一份"资源索引". */
    private String tryBuildResourceAppendix(String skillId, String guidance) {
        Path bundleRoot;
        try {
            bundleRoot = skillAssetService.ensureExtracted(skillId);
        } catch (Throwable t) {
            // bundle 不存在 / 解压失败——不阻塞 skill 读取，只是没有 appendix
            return null;
        }
        if (bundleRoot == null) return null;
        if (!Files.isDirectory(bundleRoot)) return null;

        String guidanceLower = guidance == null ? "" : guidance.toLowerCase(Locale.ROOT);
        List<String> missed = new ArrayList<>();
        try (Stream<Path> walk = Files.walk(bundleRoot, BUNDLE_WALK_MAX_DEPTH)) {
            walk.filter(Files::isRegularFile).forEach(p -> {
                if (missed.size() >= BUNDLE_WALK_MAX_ENTRIES) return; // 硬性上限防炸
                String rel = bundleRoot.relativize(p).toString().replace('\\', '/');
                if (rel.isEmpty() || rel.startsWith(".")) return;
                if ("SKILL.md".equalsIgnoreCase(rel)) return;
                // 大文件（比如 fixtures/ 里的几十 MB 样本）不该出现在索引里，
                // 模型看见也没法读，白白占 context——跳过
                try {
                    if (Files.size(p) > BUNDLE_FILE_SIZE_LIMIT) return;
                } catch (IOException ignore) { /* size 读不到就不过滤，保守列出 */ }
                // guidance 里已经提到这个路径就不重复列（简单子串匹配足够用）
                if (guidanceLower.contains(rel.toLowerCase(Locale.ROOT))) return;
                missed.add(rel);
            });
        } catch (IOException e) {
            return null;
        }
        if (missed.isEmpty()) return null;
        Collections.sort(missed);
        StringBuilder sb = new StringBuilder();
        sb.append("## 可用资源（bundle 内 SKILL.md 未提及）\n");
        sb.append("以下文件在该 skill 的 asset bundle 里。文本资源可按需用 read_skill_resource 读取；");
        sb.append("脚本类资源在 code_exec 中通过 /skill/<skillId>/... 绝对路径执行。\n");
        for (int i = 0; i < Math.min(missed.size(), BUNDLE_APPENDIX_LIST_CAP); i++) {
            sb.append("- ").append(missed.get(i)).append("\n");
        }
        if (missed.size() > BUNDLE_APPENDIX_LIST_CAP) {
            sb.append("- …（未全部列出，共发现 ").append(missed.size())
                    .append(" 个文件，仅展示前 ").append(BUNDLE_APPENDIX_LIST_CAP).append("）\n");
        }
        return sb.toString();
    }

    /** bundle 扫描目录深度上限——3 层基本覆盖 scripts/xxx/yyy.py；更深大概率是噪音. */
    private static final int BUNDLE_WALK_MAX_DEPTH = 3;
    /** 扫描到的文件条目硬上限——防御坏 bundle 里有上万个小文件把 walk 拖爆. */
    private static final int BUNDLE_WALK_MAX_ENTRIES = 500;
    /** 单文件大小上限——超过当是资源不当索引（fixtures/样本等），跳过. */
    private static final long BUNDLE_FILE_SIZE_LIMIT = 10L * 1024 * 1024;
    /** appendix 输出条数上限——列太多模型也读不过来，冗余. */
    private static final int BUNDLE_APPENDIX_LIST_CAP = 50;
    private static final int DEFAULT_SKILL_RESOURCE_LIMIT = 12000;
    private static final int MAX_SKILL_RESOURCE_LIMIT = 16000;
    private static final long SKILL_RESOURCE_MAX_BYTES = 256L * 1024;

    private static String cleanSkillResourcePath(String ref) {
        if (ref == null || ref.isEmpty()) return null;
        String s = ref.trim().replace('\\', '/');
        int hash = s.indexOf('#');
        if (hash >= 0) s = s.substring(0, hash);
        if (s.isEmpty()) return null;
        if (s.startsWith("/") || s.contains("..") || s.contains(":")) return null;
        if (s.startsWith(".")) return null;
        String lower = s.toLowerCase(Locale.ROOT);
        if ("skill.md".equals(lower)) return null;
        if (!hasAllowedSkillResourceExtension(lower)) return null;
        return s;
    }

    private static boolean hasAllowedSkillResourceExtension(String lowerPath) {
        return lowerPath.endsWith(".md")
                || lowerPath.endsWith(".txt")
                || lowerPath.endsWith(".json")
                || lowerPath.endsWith(".yml")
                || lowerPath.endsWith(".yaml")
                || lowerPath.endsWith(".toml")
                || lowerPath.endsWith(".ini")
                || lowerPath.endsWith(".cfg")
                || lowerPath.endsWith(".py")
                || lowerPath.endsWith(".js")
                || lowerPath.endsWith(".mjs")
                || lowerPath.endsWith(".ts")
                || lowerPath.endsWith(".sh")
                || lowerPath.endsWith(".css")
                || lowerPath.endsWith(".html")
                || lowerPath.endsWith(".xml");
    }

    /** 写入 skill 调用审计——失败静默（不让 telemetry 故障挡住主路径）. */
    private void recordInvocation(ToolContext ctx, String skillId, boolean duplicate) {
        try {
            SkillInvocationLog log = new SkillInvocationLog();
            log.setSkillId(skillId);
            log.setThreadId(ctx.getThreadId());
            log.setUserId(ctx.getUserId());
            log.setRunId(ctx.getRunId());
            log.setDuplicate(Boolean.valueOf(duplicate));
            log.setInvokedAt(new Date());
            invocationDao().save(log);
        } catch (Throwable t) {
            try { Tools.log("[SkillTools] recordInvocation failed", t); } catch (Throwable ignore) { /* ignore */ }
        }
    }

    /**
     * thunwind DAO 跨线程保护——详见 feedback_thunwind_dao_thread_binding.
     * tool 执行路径不一定跑在 @Inject 注入发生的线程里，直接用 {@code skillInvocationDao}
     * 会抛 {@code dao is created in a thread and used in an another thread}。
     */
    private SkillInvocationDao invocationDao() {
        try {
            SkillInvocationDao d = Tools.getBean(SkillInvocationDao.class);
            if (d != null) return d;
        } catch (Throwable ignore) { /* fallback */ }
        return skillInvocationDao;
    }

    private String asStr(Object o) { return o == null ? null : String.valueOf(o); }

    private static int asInt(Object o, int def) {
        if (o == null) return def;
        if (o instanceof Number) return ((Number) o).intValue();
        try { return Integer.parseInt(String.valueOf(o)); } catch (Exception e) { return def; }
    }

    private static String safeMsg(Throwable t) {
        if (t == null) return "";
        return t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage();
    }
}
