import { Type } from '@sinclair/typebox';
import { constants } from 'fs';
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from 'fs/promises';
import { resolveToCwd } from '../../shared/files/path-utils.js';
import { EDIT } from '../../tool-names.js';
import { withAbort } from '../../utils/abort.js';
import { enforceFilesystemPolicy } from '../permissions/filesystem-policy.js';
import { requirePermissionPolicy } from '../permissions/require-policy.js';
import { detectLineEnding, fuzzyFindText, generateDiffString, normalizeForFuzzyMatch, normalizeToLF, restoreLineEndings, stripBom } from './edit-diff.js';
const editSchema = Type.Object({
    path: Type.String({ description: 'Path to the file to edit (relative or absolute)' }),
    oldText: Type.String({ description: 'Exact text to find and replace (must match exactly)' }),
    newText: Type.String({ description: 'New text to replace the old text with' })
});
const defaultEditOperations = {
    readFile: (path) => fsReadFile(path),
    writeFile: (path, content) => fsWriteFile(path, content, 'utf-8'),
    access: (path) => fsAccess(path, constants.R_OK | constants.W_OK)
};
export function createEditTool(cwd, options) {
    const ops = options?.operations ?? defaultEditOperations;
    return {
        name: EDIT,
        label: EDIT,
        isReadOnly: false,
        description: 'Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.',
        parameters: editSchema,
        execute: async (_toolCallId, { path, oldText, newText }, signal, _onUpdate, ctx) => {
            const absolutePath = resolveToCwd(path, cwd);
            const permissionPolicy = requirePermissionPolicy(ctx, EDIT);
            await enforceFilesystemPolicy({
                path: absolutePath,
                policy: permissionPolicy.filesystem,
                access: 'write',
                sessionState: ctx?.sessionState,
                approvalGrants: ctx?.approvalGrants?.filesystem
            });
            return withAbort(signal, async (checkAborted) => {
                try {
                    await ops.access(absolutePath);
                }
                catch {
                    throw new Error(`File not found: ${path}`);
                }
                checkAborted();
                const buffer = await ops.readFile(absolutePath);
                const rawContent = buffer.toString('utf-8');
                checkAborted();
                // Strip BOM before matching (LLM won't include invisible BOM in oldText)
                const { bom, text: content } = stripBom(rawContent);
                const originalEnding = detectLineEnding(content);
                const normalizedContent = normalizeToLF(content);
                const normalizedOldText = normalizeToLF(oldText);
                const normalizedNewText = normalizeToLF(newText);
                // Find the old text using fuzzy matching (tries exact match first, then fuzzy)
                const matchResult = fuzzyFindText(normalizedContent, normalizedOldText);
                if (!matchResult.found) {
                    throw new Error(`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`);
                }
                // Count occurrences using fuzzy-normalized content for consistency
                const fuzzyContent = normalizeForFuzzyMatch(normalizedContent);
                const fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);
                const occurrences = fuzzyContent.split(fuzzyOldText).length - 1;
                if (occurrences > 1) {
                    throw new Error(`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`);
                }
                checkAborted();
                // Perform replacement using the matched text position
                const baseContent = matchResult.contentForReplacement;
                const newContent = baseContent.substring(0, matchResult.index) +
                    normalizedNewText +
                    baseContent.substring(matchResult.index + matchResult.matchLength);
                if (baseContent === newContent) {
                    throw new Error(`No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`);
                }
                const finalContent = bom + restoreLineEndings(newContent, originalEnding);
                await ops.writeFile(absolutePath, finalContent);
                // Invalidate cached read state so the next read returns full content
                ctx?.sessionState?.markModified(absolutePath);
                checkAborted();
                const diffResult = generateDiffString(baseContent, newContent);
                return {
                    content: [{ type: 'text', text: `Successfully replaced text in ${path}.` }],
                    details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine }
                };
            });
        }
    };
}
//# sourceMappingURL=edit.js.map