/**
 * ============================================================
 *  src/tools/write.ts —— Write 工具（整文件写入）
 * ------------------------------------------------------------
 *  对应 kakadeai 主仓库的 FileWriteTool。
 *  把 content 整段写到 file_path（覆盖既有内容）。
 *  目录不存在时自动创建（递归）。
 *
 *  v0.2 增强：
 *    - 路径安全校验（null byte / UNC 路径）
 *    - 内容大小上限（1GB）
 *    - 覆盖已有文件时在返回值中明确提示
 * ============================================================
 */
import { existsSync } from 'node:fs';
import { mkdir, stat, writeFile } from 'node:fs/promises';
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 { applyLineEnding, detectFileBomEncoding, detectFileLineEndings, resolveToolPath, } from '../shared/fileUtils.js';
import { filePathField } from '../shared/schemas.js';
import { assertFresh, recordRead, wasTruncated } from '../shared/fileState.js';
/** 写入内容上限：1GB。防止模型试图写入超大内容撑爆内存/磁盘。 */
const MAX_WRITE_SIZE_BYTES = 1024 * 1024 * 1024;
// ---------------- 1. Zod 输入 schema ----------------
const inputSchema = z.object({
    file_path: filePathField('写入'),
    content: z.string().describe('文件完整内容（会覆盖既有内容）'),
});
// ---------------- 2. JSON Schema（由 Zod 自动派生） ----------------
const parameters = toToolParameters(inputSchema);
// ---------------- 3. Description ----------------
const description = `Writes a file to the local filesystem.

Usage:
- This tool will overwrite the existing file if there is one at the provided path.
- If you intend to overwrite an existing file, you MUST use your \`Read\` tool to read its current content at least once in the current session BEFORE calling Write. This tool will error with a "请先 Read" message if you attempt to overwrite a file that has not been read. To create a new file (path does not yet exist), you can call Write directly without a prior Read.
- If the parent directory does not exist, it will be created recursively.
- ALWAYS prefer editing existing files in the codebase via the Edit tool. NEVER write new files unless explicitly required.
- NEVER create documentation files (*.md) or README files unless explicitly requested by the User.`;
// ---------------- 5. call() ----------------
async function call(input) {
    const pathResult = resolveToolPath(input.file_path);
    if (!pathResult.ok)
        return pathResult;
    const filePath = pathResult.resolvedPath;
    // Pre-read guard：覆盖已存在文件前必须先 Read 过；防止盲写覆盖外部修改。
    const freshness = assertFresh(filePath);
    if (!freshness.ok) {
        return { ok: false, error: freshness.error };
    }
    // Ranged-read hard guard：上次 Read 被截断时禁止 Write 覆盖。
    // Write 无脑覆盖整文件，部分 Read 后 Write 会真的把截断范围外的内容全删掉。
    if (existsSync(filePath) && wasTruncated(filePath)) {
        return {
            ok: false,
            error: `${filePath} 上次 Read 时被截断了（看到的是部分内容）。Write 会用 content 覆盖整个文件 —— 直接 Write 会丢失截断范围之外的内容！必须先用更小的 limit + 多次 offset Read 到末尾确认看完整后再 Write，或改用 Edit 工具精确替换某段内容（Edit 不需要整文件视野）。`,
        };
    }
    const contentSize = Buffer.byteLength(input.content, 'utf8');
    if (contentSize > MAX_WRITE_SIZE_BYTES) {
        return {
            ok: false,
            error: `写入内容过大（${contentSize} 字节 > ${MAX_WRITE_SIZE_BYTES} 字节 ≈ 1GB）。请拆分内容或使用 Edit 工具分块修改。`,
        };
    }
    try {
        await mkdir(dirname(filePath), { recursive: true });
        let originalSize = 0;
        const fileExisted = existsSync(filePath);
        if (fileExisted) {
            try {
                const st = await stat(filePath);
                originalSize = st.size;
            }
            catch { }
        }
        let contentToWrite = input.content;
        // bomEncoding 仅在覆盖既有文件时有意义；新建文件统一按 utf8 无 BOM 处理。
        let bomEncoding = 'utf8';
        if (fileExisted) {
            const detected = await detectFileBomEncoding(filePath);
            if (detected === 'utf16le') {
                return {
                    ok: false,
                    error: '暂不支持改写 UTF-16 LE 文件（BOM=FF FE）。请改用 UTF-8 编码。',
                };
            }
            bomEncoding = detected;
            const lineEnding = await detectFileLineEndings(filePath);
            contentToWrite = applyLineEnding(input.content, lineEnding);
        }
        if (bomEncoding === 'utf8-bom') {
            // 关键：BOM 必须走 Buffer 路径手动拼接 EF BB BF。
            // 不能直接 '\uFEFF' + content 再让 writeFile 以 utf8 编码 ——
            // 那样 utf8 编码器虽然也会输出 EF BB BF，但等价于"二次编码"，且无法保证
            // 与原文件字节级一致；显式 Buffer.concat 才能保证落盘字节稳定。
            const bomBytes = Buffer.from([0xef, 0xbb, 0xbf]);
            const bodyBytes = Buffer.from(contentToWrite, 'utf8');
            // 传 Buffer 时不要再传 encoding 参数。
            await writeFile(filePath, Buffer.concat([bomBytes, bodyBytes]));
        }
        else {
            await writeFile(filePath, contentToWrite, 'utf8');
        }
        recordRead(filePath);
        const action = fileExisted ? '已覆盖' : '已创建新文件';
        const newSize = Buffer.byteLength(contentToWrite, 'utf8');
        const sizeInfo = fileExisted
            ? `（原文件 ${originalSize} 字节 → 新内容 ${newSize} 字节）`
            : `（${newSize} 字节）`;
        const bomInfo = bomEncoding === 'utf8-bom' ? '，已保留 UTF-8 BOM' : '';
        return {
            ok: true,
            content: `${action} ${filePath}${sizeInfo}${bomInfo}`,
        };
    }
    catch (e) {
        return { ok: false, error: `写入失败：${e.message}` };
    }
}
// ---------------- 6. 导出 ----------------
export const writeTool = {
    name: 'Write',
    description,
    inputSchema,
    parameters,
    isReadOnly: false,
    isConcurrencySafe: false,
    maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
    call,
};
