/**
 * ============================================================
 *  src/tools/read.ts —— Read 工具（读文件）
 * ------------------------------------------------------------
 *  对应 kakadeai 主仓库的 FileReadTool。
 *  prompt 文本和 Zod schema 与 kakadeai 一致，让"模型可见接口"完全对齐；
 *  call() 实现用最简单的 fs/promises，剥离了 UI/权限/PDF/Notebook/图片支持。
 *
 *  输入：
 *    file_path  必填，绝对路径（建议）或相对项目根
 *    offset     可选，1-indexed 起始行号
 *    limit      可选，最大读多少行（默认 2000）
 *
 *  输出：
 *    带行号前缀的文本（格式："${line}\t${content}"），与 Read 工具的 prompt 约定一致
 * ============================================================
 */
import { createReadStream } from 'node:fs';
import { readFile, stat } from 'node:fs/promises';
import { createInterface } from 'node:readline';
import { z } from 'zod';
import { DEFAULT_MAX_RESULT_SIZE_CHARS, MAX_FILE_SIZE_BYTES, MAX_LINES_TO_READ } from '../types.js';
import { toToolParameters } from '../../utils/zodToJson.js';
import { hasBinaryExtension, isBlockedDevicePath, resolveToolPath } from '../shared/fileUtils.js';
import { filePathField } from '../shared/schemas.js';
import { recordRead } from '../shared/fileState.js';
// ---------------- 1. Zod 输入 schema ----------------
const inputSchema = z.object({
    file_path: filePathField('读取'),
    offset: z
        .number()
        .int()
        .positive()
        .optional()
        .describe('起始行号（1-indexed）；不填则从文件开头读'),
    limit: z
        .number()
        .int()
        .positive()
        .optional()
        .describe(`最多读多少行；不填则用默认值 ${MAX_LINES_TO_READ}`),
});
// ---------------- 2. JSON Schema（给 LLM 看） ----------------
//
// 与上面的 Zod schema 保持一致；改动时两边都要改。
// ---------------- 2. JSON Schema（由 Zod 自动派生） ----------------
const parameters = toToolParameters(inputSchema);
// ---------------- 3. Description（给 LLM 看的工具说明） ----------------
//
// 这段文本直接搬自 kakadeai/tools/FileReadTool/prompt.ts 的 DESCRIPTION，
// 只是去掉了和 PDF/Notebook/图片相关的能力描述（最小版不支持）。
const description = `Reads a file from the local filesystem. You can access any file directly by using this tool.
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.

Usage:
- The file_path parameter is preferably an absolute path. Relative paths are accepted and will be resolved against the agent's working directory (locked at startup via \`-d <dir>\` or process cwd).
- By default, it reads up to ${MAX_LINES_TO_READ} lines starting from the beginning of the file
- You can optionally specify a line offset and limit (especially handy for long files)
- Results are returned using cat -n format, with line numbers starting at 1
- This tool can only read text files, not directories. To read a directory, use the Glob tool.
- If you read a file that exists but has empty contents you will receive a warning in place of file contents.
- This tool cannot read binary files. Files whose extensions are on the binary blocklist (images: .png/.jpg/.gif/.webp/..., documents: .pdf/.docx/.xlsx/..., executables: .exe/.dll/.so/..., archives: .zip/.tar/.gz/..., and others) will be rejected with a clear error. If the file is actually text despite the extension (e.g., a misnamed log), rename it or use Bash \`cat\` to read it directly — do not retry Read with the same path.`;
// ---------------- 4. call() —— 真正干活 ----------------
const STREAM_THRESHOLD = 1024 * 1024;
/** "同区重读空转"提示：同一区间 + 文件未变 + 第二次起，前置到结果头部（仅提示，不拦截）。 */
const REREAD_HINT = '⚠️ 你本轮已经读过该文件的这一区间，且文件未发生变化——请直接基于上次读到的内容操作，不要重复读取空转。如果你其实在找别的内容，改用不同的 offset/limit，或用 Grep 精确定位。\n\n';
async function call(input) {
    const offset = input.offset ?? 1;
    const limit = input.limit ?? MAX_LINES_TO_READ;
    // 4a. 路径规范化 + 安全校验（替换原来的手动 resolve + validatePath）
    const pathResult = resolveToolPath(input.file_path);
    if (!pathResult.ok)
        return pathResult;
    const filePath = pathResult.resolvedPath;
    // 4b. 设备文件双重拦截（虽然 validateAndResolvePath 已包含，但双重保险）
    if (isBlockedDevicePath(filePath)) {
        return { ok: false, error: `不允许读取设备文件：${filePath}。该路径可能产生无限输出或阻塞进程。` };
    }
    // 4b'. 二进制扩展名黑名单：在 stat 之前快速短路，避免把 .png/.exe/.pdf 等塞回 LLM
    if (hasBinaryExtension(filePath)) {
        return {
            ok: false,
            error: `不支持读取二进制文件：${filePath}（扩展名命中二进制黑名单）。若该文件实际为文本，可改后缀或用 Bash cat 旁路。`,
        };
    }
    // 4c. 文件存在性 / 大小检查
    let st;
    try {
        st = await stat(filePath);
    }
    catch (e) {
        return {
            ok: false,
            error: `读取失败：${filePath} 不存在或无法访问（${e.message}）`,
        };
    }
    if (!st.isFile()) {
        return { ok: false, error: `${filePath} 不是文件（可能是目录）。要列目录用 Glob 工具。` };
    }
    if (st.size > MAX_FILE_SIZE_BYTES) {
        return {
            ok: false,
            error: `文件过大（${st.size} 字节 > ${MAX_FILE_SIZE_BYTES}）。请用 offset/limit 分段读。`,
        };
    }
    // 4d. 根据文件大小选择读取策略
    let numbered;
    let totalLines;
    if (st.size <= STREAM_THRESHOLD) {
        // 小文件：原有 readFile 路径
        const result = await readSmallFile(filePath, offset, limit);
        numbered = result.numbered;
        totalLines = result.totalLines;
        if (result.isEmpty) {
            recordRead(filePath);
            return { ok: true, content: '<file is empty>' };
        }
    }
    else {
        // 大文件（>1MB 且 <= MAX_FILE_SIZE_BYTES）：流式读取
        const result = await readLargeFileStream(filePath, offset, limit);
        numbered = result.numbered;
        totalLines = result.totalLines;
    }
    // 4e. 空文件处理
    if (totalLines === 0 || !numbered) {
        recordRead(filePath);
        return { ok: true, content: '<file is empty>' };
    }
    // 4f. 大小兜底 + 自动分段提示
    let content = numbered;
    let truncated = false;
    if (content.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
        content =
            content.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) +
                `\n\n... (输出超过 ${DEFAULT_MAX_RESULT_SIZE_CHARS} 字符，已截断)` +
                `\n\n⚠️ **重要：本次返回的是截断内容**。直接基于这部分内容写 Edit/Write 会失败 —— old_string 可能在截断范围外，Write 会丢失截断范围外的内容。必须做以下之一：(1) 用更小的 limit + 精确的 offset 缩小读取范围；(2) 已知改动位置时直接 offset 到目标行号附近；(3) 整文件覆盖请用多次 Edit 而非 Write。`;
        truncated = true;
    }
    const startIdx = Math.max(0, offset - 1);
    const endIdx = Math.min(totalLines, startIdx + limit);
    if (endIdx < totalLines) {
        const nextOffset = endIdx + 1;
        const returnedLines = content.split('\n').filter(l => l.trim()).length;
        content += `\n\n... (本次返回 ${returnedLines} 行 / 文件共 ${totalLines} 行；用 offset=${nextOffset} 继续读)`;
    }
    if (st.size > 100 * 1024 && offset === 1) {
        content += `\n\n⚠️ 注意：这是一个大文件（${(st.size / 1024).toFixed(1)} KB）。建议用 offset/limit 分段读取，例如先读关键部分（imports、exports、函数签名）。`;
    }
    const readCount = recordRead(filePath, { truncated, offset, limit });
    if (readCount >= 2)
        content = REREAD_HINT + content;
    return { ok: true, content };
}
async function readSmallFile(filePath, offset, limit) {
    const raw = await readFile(filePath, 'utf8');
    if (raw.length === 0) {
        return { numbered: '', totalLines: 0, isEmpty: true };
    }
    const allLines = raw.split('\n');
    const totalLines = allLines.length;
    const startIdx = Math.max(0, offset - 1);
    const endIdx = Math.min(totalLines, startIdx + limit);
    const slice = allLines.slice(startIdx, endIdx);
    const numbered = slice
        .map((line, i) => `${(startIdx + i + 1).toString()}\t${line}`)
        .join('\n');
    return { numbered, totalLines, isEmpty: false };
}
async function readLargeFileStream(filePath, offset, limit) {
    return new Promise((resolvePromise, reject) => {
        const lines = [];
        let currentLine = 0;
        const startIdx = Math.max(0, offset - 1);
        const endLine = startIdx + limit;
        const input = createReadStream(filePath, { encoding: 'utf8' });
        const rl = createInterface({
            input,
            crlfDelay: Infinity,
        });
        input.on('error', (err) => {
            reject(err);
        });
        rl.on('line', (line) => {
            currentLine++;
            if (currentLine > endLine) {
                rl.close();
                rl.removeAllListeners();
                return;
            }
            if (currentLine >= offset) {
                lines.push(`${currentLine}\t${line}`);
            }
        });
        rl.on('close', () => {
            resolvePromise({
                numbered: lines.join('\n'),
                totalLines: currentLine,
            });
        });
        rl.on('error', (err) => {
            reject(err);
        });
    });
}
// ---------------- 5. 导出工具对象 ----------------
export const readTool = {
    name: 'Read',
    description,
    inputSchema,
    parameters,
    isReadOnly: true,
    isConcurrencySafe: true,
    maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
    call,
};
