/**
 * ============================================================
 *  src/tools/edit.ts —— Edit 工具（精确字符串替换）
 * ------------------------------------------------------------
 *  对应 kakadeai 主仓库的 FileEditTool。
 *  prompt 文本和 Zod schema 与 kakadeai 一致。
 *
 *  v0.2 增强：
 *    - 路径安全校验（null byte / UNC 路径）
 *    - 文件大小上限（1GB）
 *    - old_string 找不到时提供模糊匹配提示（最常见模型错误）
 * ============================================================
 */
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { dirname } from 'node:path';
import { z } from 'zod';
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
import { toToolParameters } from '../../utils/zodToJson.js';
import { resolveToolPath, findActualString, preserveQuoteStyle, detectLineEndingsForString, applyLineEnding, countOccurrences, splitReplaceAll, tryAutoSnapshot, } from '../shared/fileUtils.js';
import { filePathField } from '../shared/schemas.js';
import { assertFresh, recordRead, wasTruncated } from '../shared/fileState.js';
/** 编辑文件大小上限：1GB。防止模型试图 Edit 超大文件撑爆内存。 */
const MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
// ---------------- 1. Zod 输入 schema ----------------
const inputSchema = z.object({
    file_path: filePathField('编辑'),
    old_string: z
        .string()
        .describe('要替换的原文本（必须在文件中唯一，除非 replace_all=true）；为空字符串且文件不存在时创建新文件'),
    new_string: z.string().describe('替换为的新文本（与 old_string 必须不同）'),
    replace_all: z
        .boolean()
        .optional()
        .describe('是否替换所有出现位置（默认 false）'),
});
// ---------------- 2. JSON Schema（由 Zod 自动派生） ----------------
const parameters = toToolParameters(inputSchema);
// ---------------- 3. Description（搬自 kakadeai/tools/FileEditTool/prompt.ts） ----------------
const description = `Performs exact string replacements in files.

Usage:
- You MUST use your \`Read\` tool to read the current content of the file BEFORE calling Edit.
  Memory is unreliable — if you think you "remember" how the code looks, you are probably wrong.
  Re-read the relevant sections (function, module, or area you plan to change) every time.
- To create a new file, pass an empty \`old_string\` and the desired contents as \`new_string\`. When creating a new file (path does not yet exist), you may call Edit directly — no prior Read is needed.
- If the file already exists and you pass an empty \`old_string\`, the edit is rejected with a clear error. To replace the entire content of an existing file, use the Write tool instead.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
- Preserve exact indentation (tabs/spaces).`;
// ---------------- 4. call() ----------------
async function call(input) {
    const { old_string, new_string } = input;
    const replaceAll = input.replace_all ?? false;
    // 5a. 路径规范化 + 安全校验（含 null byte / UNC / 设备文件拦截 / ~ 展开）
    const pathResult = resolveToolPath(input.file_path);
    if (!pathResult.ok)
        return pathResult;
    const filePath = pathResult.resolvedPath;
    // 5a-bis. Pre-read guard：文件已存在则必须先 Read（创建新文件场景下 assertFresh 返回 ok:true）
    // 拦截路径自动注入 snippet：让模型在同一轮 tool result 里看到当前内容，无需多绕一次 Read。
    // 安全语义保留：模型必须显式重发 Edit 才会落盘；snippet 只是给模型看的，不静默放行。
    const freshness = assertFresh(filePath);
    if (!freshness.ok) {
        const snap = await tryAutoSnapshot(filePath);
        if (snap.ok) {
            // 关键：truncated=true 时 wasTruncated 会守住 Write 路径，防止基于残缺 snippet 重写整文件
            recordRead(filePath, { truncated: snap.truncated });
            const tail = snap.truncated
                ? `\n\n⚠️ 仅注入了前 200 行（文件共 ${snap.totalLines} 行 / 上限 32KB）。若要改的位置在范围外，请显式 Read offset=X limit=Y 后再 Edit。`
                : '';
            return {
                ok: false,
                error: freshness.error +
                    `\n\n以下是当前文件${snap.truncated ? '前 200 行' : '全部'}内容（带行号），请基于此重新构造 old_string 后立即重试 Edit，不要再单独调 Read：\n\n${snap.snippet}` +
                    tail,
            };
        }
        const why = snap.reason === 'too-large'
            ? `文件 ${((snap.sizeBytes ?? 0) / 1024).toFixed(1)} KB 超过 32KB 自动注入上限`
            : snap.reason === 'binary'
                ? '文件命中二进制扩展名黑名单'
                : '文件读取失败';
        return {
            ok: false,
            error: `${freshness.error}\n\n（${why}，未注入内容快照，请用 Read 工具显式读取后再 Edit）`,
        };
    }
    if (old_string === new_string) {
        return { ok: false, error: 'old_string 与 new_string 完全相同，没有可改的内容。' };
    }
    // 5b. 创建新文件路径：old_string === '' 且文件不存在
    if (old_string === '' && !existsSync(filePath)) {
        try {
            await mkdir(dirname(filePath), { recursive: true });
            await writeFile(filePath, new_string, 'utf8');
            recordRead(filePath);
            return {
                ok: true,
                content: `已创建新文件：${filePath}（${new_string.length} 字符）`,
            };
        }
        catch (e) {
            return { ok: false, error: `创建文件失败：${e.message}` };
        }
    }
    // 5c. 既有文件：读 → 改 → 写
    if (!existsSync(filePath)) {
        return {
            ok: false,
            error: `文件不存在：${filePath}\n（要创建新文件，请把 old_string 设为空字符串）`,
        };
    }
    let original;
    try {
        original = await readFile(filePath, 'utf8');
    }
    catch (e) {
        return { ok: false, error: `读取失败：${e.message}` };
    }
    // 5d-1. 记录原文件行尾符风格（写入时保持一致）
    const originalLineEnding = detectLineEndingsForString(original);
    // 5d. 文件大小上限检查
    const fileSize = Buffer.byteLength(original, 'utf8');
    if (fileSize > MAX_EDIT_FILE_SIZE_BYTES) {
        return {
            ok: false,
            error: `文件过大（${fileSize} 字节 > ${MAX_EDIT_FILE_SIZE_BYTES} 字节 ≈ 1GB）。Edit 不适合操作超大文件，请用 Write 工具。`,
        };
    }
    // 唯性检查
    if (old_string === '') {
        return {
            ok: false,
            error: 'old_string 为空但文件已存在 —— 这通常是错误的。要替换全文请用 Write 工具。',
        };
    }
    // 5e. 引号风格归一化：处理模型输出的直引号 vs 文件中的弯引号差异
    let searchTarget = old_string;
    let processedNewString = new_string;
    const actualOld = findActualString(original, old_string);
    if (actualOld !== null && actualOld !== old_string) {
        searchTarget = actualOld;
        processedNewString = preserveQuoteStyle(old_string, actualOld, new_string);
    }
    // splitCount = 出现次数
    const occurrences = countOccurrences(original, searchTarget);
    const truncHint = wasTruncated(filePath)
        ? `\n\n⚠️ 上次 Read 该文件时被截断了（只看到部分内容）。当前 old_string 可能在截断范围外。请用更小的 limit + offset 精确读取目标行号附近后再 Edit。`
        : '';
    if (occurrences === 0) {
        // 模糊匹配提示：找找有没有"接近"的内容
        const hint = findFuzzyMatchHint(original, searchTarget);
        const extraMsg = hint ? `\n\n💡 提示：${hint}` : '';
        return {
            ok: false,
            error: `在 ${filePath} 中找不到 old_string。请先用 Read 工具核对内容（注意空格/缩进/换行）。${extraMsg}${truncHint}`,
        };
    }
    if (occurrences > 1 && !replaceAll) {
        return {
            ok: false,
            error: `old_string 在文件中出现了 ${occurrences} 次，不唯一。\n请扩大 old_string 包含更多上下文，或显式传 replace_all=true。${truncHint}`,
        };
    }
    const replaced = replaceAll
        ? splitReplaceAll(original, searchTarget, processedNewString)
        : original.replace(searchTarget, processedNewString); // 只替换第一处（已确认 occurrences === 1）
    // 5f. 保持原文件行尾符风格
    const normalizedReplaced = applyLineEnding(replaced, originalLineEnding);
    try {
        await writeFile(filePath, normalizedReplaced, 'utf8');
        recordRead(filePath);
    }
    catch (e) {
        return { ok: false, error: `写入失败：${e.message}` };
    }
    // 输出：报告改了几处 + 行变化
    const linesBefore = original.split('\n').length;
    const linesAfter = replaced.split('\n').length;
    return {
        ok: true,
        content: `已编辑 ${filePath}\n` +
            `替换次数：${replaceAll ? occurrences : 1}\n` +
            `行数：${linesBefore} → ${linesAfter}`,
    };
}
// ---------------- 5. 模糊匹配提示 ----------------
/**
 * 当 old_string 精确匹配失败时，尝试在文件内容中找到相似行。
 * 常见场景：模型传入的 old_string 缩进差一个 tab、末尾多一个空格等。
 *
 * 策略：
 *   1. 按 \n 分割成行
 *   2. 对每行计算与 old_string 的字符重叠率（最长公共子序列长度 / max(len)）
 *   3. 返回重叠率最高的前 N 行作为提示
 */
function findFuzzyMatchHint(fileContent, target) {
    const MIN_OVERLAP_RATIO = 0.5; // 至少 50% 字符重叠才认为"接近"
    const MAX_HINTS = 3; // 最多提示 3 个候选
    const targetTrimmed = target.trim();
    if (targetTrimmed.length < 5)
        return null; // 太短的 target 不做模糊匹配
    const lines = fileContent.split('\n');
    const candidates = [];
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        const trimmedLine = line.trim();
        // 跳过空行和太短的行
        if (trimmedLine.length < 5)
            continue;
        const ratio = lcsRatio(targetTrimmed, trimmedLine);
        if (ratio >= MIN_OVERLAP_RATIO) {
            candidates.push({ lineNum: i + 1, text: trimmedLine, ratio });
        }
    }
    // 也检查跨行情况：如果 target 包含换行，尝试匹配连续多行
    if (target.includes('\n') && candidates.length < MAX_HINTS) {
        const targetLines = target.split('\n').filter((l) => l.trim().length > 0);
        if (targetLines.length >= 2) {
            for (let start = 0; start <= lines.length - targetLines.length; start++) {
                const window = lines.slice(start, start + targetLines.length).map((l) => l.trim()).join('\n');
                const ratio = lcsRatio(target.trim(), window);
                if (ratio >= MIN_OVERLAP_RATIO) {
                    candidates.push({ lineNum: start + 1, text: window.slice(0, 80) + (window.length > 80 ? '...' : ''), ratio });
                }
            }
        }
    }
    if (candidates.length === 0)
        return null;
    // 按重叠率降序排列，取前 N 个
    candidates.sort((a, b) => b.ratio - a.ratio);
    const top = candidates.slice(0, MAX_HINTS);
    const hints = top.map((c) => `第 ${c.lineNum} 行附近有相似内容（相似度 ${(c.ratio * 100).toFixed(0)}%）:\n      "${c.text.slice(0, 100)}${c.text.length > 100 ? '…' : ''}"`);
    return `可能你想要修改以下位置之一？\n  ${hints.join('\n  ')}`;
}
/**
 * 计算两个字符串的最长公共子序列（LCS）长度比。
 * 使用动态规划，时间复杂度 O(m*n)，但对短字符串足够快。
 * 返回值范围 [0, 1]，1 表示完全相同。
 */
function lcsRatio(a, b) {
    const m = a.length;
    const n = b.length;
    // 对太长的字符串截断处理（防止性能问题）
    const MAX_LEN = 500;
    const aa = m > MAX_LEN ? a.slice(0, MAX_LEN) : a;
    const bb = n > MAX_LEN ? b.slice(0, MAX_LEN) : b;
    const mm = aa.length;
    const nn = bb.length;
    // 用一维 DP 优化空间
    const dp = new Array(nn + 1).fill(0);
    for (let i = 1; i <= mm; i++) {
        let prev = 0;
        for (let j = 1; j <= nn; j++) {
            const temp = dp[j];
            if (aa[i - 1] === bb[j - 1]) {
                dp[j] = prev + 1;
            }
            else {
                dp[j] = Math.max(dp[j], dp[j - 1]);
            }
            prev = temp;
        }
    }
    return dp[nn] / Math.max(mm, nn);
}
// ---------------- 6. 导出 ----------------
export const editTool = {
    name: 'Edit',
    description,
    inputSchema,
    parameters,
    isReadOnly: false,
    isConcurrencySafe: false,
    maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
    call,
};
