/**
 * ============================================================
 *  src/tools/edit/multi-edit.ts —— MultiEdit 工具（原子化多处替换）
 * ------------------------------------------------------------
 *  对应 kakadeai 主仓库的 FileEditTool.getPatchForEdits 设计思路，
 *  但比上游更严格：**all-or-nothing**。
 *
 *  与 Edit 的关键差异：
 *    1. 一次处理多条 edit，按顺序在内存中应用，全部成功才写盘
 *    2. 任何一条匹配失败 / 多义匹配 → 全部回退（磁盘不动一字）
 *    3. 依赖检测：edits[i].old_string 若是 edits[j<i].new_string 的子串，
 *       会命中前序产物，直接拒绝（要求调用方重排序或合并）
 *    4. 不支持 "old_string='' 创建文件"（那是 Edit 的单文件场景，多 edit 无意义）
 * ============================================================
 */
import { readFile, writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { z } from 'zod';
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
import { toToolParameters } from '../../utils/zodToJson.js';
import { resolveToolPath, detectFileLineEndings, applyLineEnding, findActualString, preserveQuoteStyle, countOccurrences, splitReplaceAll, tryAutoSnapshot, } from '../shared/fileUtils.js';
import { filePathField } from '../shared/schemas.js';
import { assertFresh, recordRead, wasTruncated } from '../shared/fileState.js';
// ---------------- 1. Zod 输入 schema ----------------
const editItemSchema = z.object({
    old_string: z.string().min(1).describe('要替换的原文本（不允许为空 —— 创建新文件请用 Edit 工具）'),
    new_string: z.string().describe('替换为的新文本'),
    replace_all: z
        .boolean()
        .optional()
        .describe('是否替换所有出现位置（默认 false，要求 old_string 在当前内容中唯一）'),
});
const inputSchema = z.object({
    file_path: filePathField('编辑'),
    edits: z
        .array(editItemSchema)
        .min(1)
        .max(50)
        .describe('按顺序应用的 edit 列表（1-50 条），原子化执行：全部成功才落盘'),
});
// ---------------- 2. JSON Schema ----------------
const parameters = toToolParameters(inputSchema);
// ---------------- 3. Description ----------------
const description = `Performs multiple exact string replacements in a single file, applied atomically (all-or-nothing).

Usage:
- You MUST use your \`Read\` tool to read the current content of the file BEFORE calling MultiEdit.
- Provide a list of \`edits\`, each with \`old_string\`, \`new_string\`, and optional \`replace_all\`.
- All edits are applied sequentially in memory; if ANY edit fails (string not found, ambiguous match, or dependency conflict), the file on disk is UNTOUCHED.
- Order matters: later edits operate on the result of earlier edits. If a later edit's \`old_string\` is a substring of an earlier edit's \`new_string\`, MultiEdit refuses (reorder or merge instead).
- Each \`old_string\` must be unique in the current content (after prior edits) unless \`replace_all=true\`.
- Empty \`old_string\` is NOT allowed in MultiEdit. To create a new file, use the \`Edit\` tool with a single empty-old_string call.
- Preserve exact indentation (tabs/spaces).`;
// ---------------- 4. call() ----------------
async function call(input) {
    // 4a. 路径规范化 + 安全校验
    const pathResult = resolveToolPath(input.file_path);
    if (!pathResult.ok)
        return pathResult;
    const filePath = pathResult.resolvedPath;
    // 4b. Pre-read guard —— 入口处一次性检查 + 注入 snippet（循环内多个 edit 不重复注入）
    const freshness = assertFresh(filePath);
    if (!freshness.ok) {
        const snap = await tryAutoSnapshot(filePath);
        if (snap.ok) {
            recordRead(filePath, { truncated: snap.truncated });
            const tail = snap.truncated
                ? `\n\n⚠️ 仅注入了前 200 行（文件共 ${snap.totalLines} 行 / 上限 32KB）。若要改的位置在范围外，请显式 Read offset=X limit=Y 后再 MultiEdit。`
                : '';
            return {
                ok: false,
                error: freshness.error +
                    `\n\n以下是当前文件${snap.truncated ? '前 200 行' : '全部'}内容（带行号），请基于此重新构造各 edit 的 old_string 后立即重试 MultiEdit，不要再单独调 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 工具显式读取后再 MultiEdit）`,
        };
    }
    // 4c. 文件必须存在（MultiEdit 不做"创建新文件"语义）
    if (!existsSync(filePath)) {
        return {
            ok: false,
            error: `文件不存在：${filePath}\n（MultiEdit 不支持创建新文件，请改用 Edit 工具）`,
        };
    }
    // 4d. 依赖检测：edits[i].old_string 若是 edits[j<i].new_string 的子串，
    //     后续 edit 会命中前序产物，直接拒绝。
    const edits = input.edits;
    for (let i = 0; i < edits.length; i++) {
        for (let j = 0; j < i; j++) {
            if (edits[j].new_string.includes(edits[i].old_string)) {
                return {
                    ok: false,
                    error: `edits[${i}].old_string 是 edits[${j}].new_string 的子串，会导致后续 edit 命中前序产物，请重新排序或合并`,
                };
            }
        }
    }
    // 4e. 读原文
    let originalContent;
    try {
        originalContent = await readFile(filePath, 'utf8');
    }
    catch (e) {
        return { ok: false, error: `读取失败：${e.message}` };
    }
    // 4f. 在内存中顺序应用所有 edit（任一失败立即返回，磁盘不动）
    let currentContent = originalContent;
    const truncHint = wasTruncated(filePath)
        ? `\n\n⚠️ 上次 Read 该文件时被截断了（只看到部分内容）。当前 old_string 可能在截断范围外。请用更小的 limit + offset 精确读取目标行号附近后再 MultiEdit。`
        : '';
    for (let i = 0; i < edits.length; i++) {
        const edit = edits[i];
        const replaceAll = edit.replace_all ?? false;
        // 引号风格归一化
        let searchTarget = edit.old_string;
        let processedNewString = edit.new_string;
        const actualOld = findActualString(currentContent, edit.old_string);
        if (actualOld === null) {
            return {
                ok: false,
                error: `edits[${i}] 未匹配到 old_string（在已应用前序 ${i} 处修改后的内容中找不到）。请先用 Read 工具核对当前内容（注意空格/缩进/换行），并检查 edit 顺序。${truncHint}`,
            };
        }
        if (actualOld !== edit.old_string) {
            searchTarget = actualOld;
            processedNewString = preserveQuoteStyle(edit.old_string, actualOld, edit.new_string);
        }
        const occurrences = countOccurrences(currentContent, searchTarget);
        if (occurrences === 0) {
            return {
                ok: false,
                error: `edits[${i}] 未匹配到 old_string（已应用前序 ${i} 处修改后内容中出现 0 次）。${truncHint}`,
            };
        }
        if (occurrences > 1 && !replaceAll) {
            return {
                ok: false,
                error: `edits[${i}].old_string 在当前内容中出现 ${occurrences} 次，不唯一。请扩大 old_string 包含更多上下文，或显式传 replace_all=true。${truncHint}`,
            };
        }
        currentContent = replaceAll
            ? splitReplaceAll(currentContent, searchTarget, processedNewString)
            : currentContent.replace(searchTarget, processedNewString);
    }
    // 4g. 全部成功 → 保持原文件行尾符风格，写盘
    const lineEnding = await detectFileLineEndings(filePath);
    const finalContent = applyLineEnding(currentContent, lineEnding);
    try {
        await writeFile(filePath, finalContent, 'utf8');
        recordRead(filePath);
    }
    catch (e) {
        return { ok: false, error: `写入失败：${e.message}` };
    }
    return {
        ok: true,
        content: `已对 ${filePath} 应用 ${edits.length} 处修改`,
    };
}
// ---------------- 5. 导出 ----------------
export const multiEditTool = {
    name: 'MultiEdit',
    description,
    inputSchema,
    parameters,
    isReadOnly: false,
    isConcurrencySafe: false,
    maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
    call,
};
