package com.gzzm.lobster.tool.builtin;

import com.gzzm.lobster.common.ToolCategory;
import com.gzzm.lobster.common.ToolRiskLevel;
import com.gzzm.lobster.storage.FileSystemContentStore;
import com.gzzm.lobster.tool.BuiltinToolDefinition;
import com.gzzm.lobster.tool.SchemaBuilder;
import com.gzzm.lobster.tool.ToolContext;
import com.gzzm.lobster.tool.ToolExecutor;
import com.gzzm.lobster.tool.ToolRegistry;
import com.gzzm.lobster.tool.ToolResult;
import com.gzzm.platform.commons.Tools;
import net.cyan.nest.annotation.Inject;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * ReadToolResultTool —— 按后端 ContentStore ref 读取外置内容 /
 * Fetch externalized content via its backend-generated ContentStore ref.
 *
 * <p>用途：当 send-view 里看到形如 {@code <truncated ... ref=message/...>} 的截断标记，
 * 模型可把标记里的 ref 原样传给本工具，按 (ref, offset, limit) 拉子段，避开把全文塞回上下文。
 * 这不是按工具名/skillId 回查历史结果的工具；读取 skill 方法论必须调用 {@code use_skill(skillId)}。
 *
 * <p>权限：仅按 ref 读 ContentStore，无写副作用；按 {@link com.gzzm.lobster.common.ToolRiskLevel#READ_ONLY} 注册。安全考虑：
 * ref 是后端生成的不透明路径，模型只能读已经被合法外置过的内容（任意 path 也会被
 * {@link FileSystemContentStore#read} 内部的路径穿越防护拦下）。
 */
public class ReadToolResultTool implements ToolExecutor {

    /** 单次回查的字符上限，避免模型一次性把全文拉回上下文（与初始截断政策对齐）. */
    private static final int DEFAULT_LIMIT = 8000;
    private static final int MAX_LIMIT = 16000;

    @Inject private FileSystemContentStore contentStore;

    public void registerTo(ToolRegistry registry) {
        registry.register(def(), this);
    }

    private BuiltinToolDefinition def() {
        return BuiltinToolDefinition.builder()
                .name("read_externalized_content")
                .displayName("读取外置内容片段")
                .description("只用于读取系统在上下文里明确标出的外置内容片段。"
                        + "仅当当前可见消息中已经出现后端生成的截断/外置标记（形如 'ref=...'）时使用，"
                        + "并且必须把标记里的 ref 原样传入。"
                        + "不要按工具名、toolCallId、skillId 或 artifactId 自己拼 ref；"
                        + "不要用本工具读取 skill 文档、详细指南、文件、artifact 或历史工具调用结果。"
                        + "需要读取 Skill 方法论或更多 Skill 细节时，只能调用 use_skill(skillId)。"
                        + "\n\n【典型场景】"
                        + "\n- 当前可见内容出现：<truncated ...; ref=message/...; ...>"
                        + "\n- 需要该外置内容的中间片段："
                        + "  read_externalized_content(ref='<标记里的精确 ref>', offset=8000, limit=8000)"
                        + "\n\n参数：offset 字符位置（默认 0）；limit 单次最多 16000 字符（默认 8000）。")
                .category(ToolCategory.WORKSPACE)
                .risk(ToolRiskLevel.READ_ONLY)
                .inputSchema(SchemaBuilder.obj()
                        .prop("ref", "string", "后端截断/外置标记里的精确 ref 值；不要自行构造")
                        .propInt("offset", "起始字符位置；缺省 0")
                        .propInt("limit", "本次读取的最大字符数；缺省 8000，硬上限 16000")
                        .required("ref")
                        .build())
                .build();
    }

    @Override
    public ToolResult execute(ToolContext ctx, Map<String, Object> args) throws Exception {
        String ref = asStr(args.get("ref"));
        if (ref == null || ref.isEmpty()) {
            return ToolResult.error("read_externalized_content.invalid: missing ref");
        }
        int offset = Math.max(0, asInt(args.get("offset"), 0));
        int limit = asInt(args.get("limit"), DEFAULT_LIMIT);
        if (limit <= 0) limit = DEFAULT_LIMIT;
        if (limit > MAX_LIMIT) limit = MAX_LIMIT;

        String body;
        long fullSize;
        try {
            if (!contentStore.exists(ref)) {
                return ToolResult.error("read_externalized_content.not_found: " + ref
                        + "；ref 必须来自截断/外置标记里的精确值。"
                        + "不要用本工具读取 skill、指南或历史工具结果；读取 skill 请调用 use_skill(skillId)。");
            }
            fullSize = contentStore.getSize(ref);
            body = contentStore.read(ref, offset, limit);
        } catch (Throwable t) {
            try { Tools.log("[ReadToolResultTool] read failed ref=" + ref, t); }
            catch (Throwable ignore) { /* ignore */ }
            String cause = t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage();
            return ToolResult.error("read_externalized_content.io_error: " + cause);
        }

        Map<String, Object> data = new LinkedHashMap<>();
        data.put("ref", ref);
        data.put("offset", offset);
        data.put("limit", limit);
        data.put("returned_chars", body == null ? 0 : body.length());
        data.put("full_size_bytes", fullSize);
        data.put("body", body == null ? "" : body);
        return ToolResult.ok("ok", data);
    }

    private static 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; }
    }
}
