import { createTwoFilesPatch } from "diff";
import z from "zod";
import { createToolFactory, ToolCategory, ToolExecutionError, } from "../tools2/types.js";
import { debugCache, createTypeScriptCheckFunction } from "./debug-cache.js";
// Similarity thresholds for block anchor fallback matching
const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0;
const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3;
/**
 * Levenshtein distance algorithm implementation
 */
function levenshtein(a, b) {
    // Handle empty strings
    if (a === "" || b === "") {
        return Math.max(a.length, b.length);
    }
    const matrix = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
    for (let i = 1; i <= a.length; i++) {
        for (let j = 1; j <= b.length; j++) {
            const cost = a[i - 1] === b[j - 1] ? 0 : 1;
            const currentRow = matrix[i];
            const prevRow = matrix[i - 1];
            currentRow[j] = Math.min(prevRow[j] + 1, currentRow[j - 1] + 1, prevRow[j - 1] + cost);
        }
    }
    return matrix[a.length][b.length];
}
export const SimpleReplacer = function* (_content, find) {
    yield find;
};
export const LineTrimmedReplacer = function* (content, find) {
    const originalLines = content.split("\n");
    const searchLines = find.split("\n");
    if (searchLines[searchLines.length - 1] === "") {
        searchLines.pop();
    }
    for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
        let matches = true;
        for (let j = 0; j < searchLines.length; j++) {
            const originalTrimmed = originalLines[i + j]?.trim() ?? "";
            const searchTrimmed = searchLines[j]?.trim() ?? "";
            if (originalTrimmed !== searchTrimmed) {
                matches = false;
                break;
            }
        }
        if (matches) {
            let matchStartIndex = 0;
            for (let k = 0; k < i; k++) {
                matchStartIndex += (originalLines[k]?.length ?? 0) + 1;
            }
            let matchEndIndex = matchStartIndex;
            for (let k = 0; k < searchLines.length; k++) {
                matchEndIndex += originalLines[i + k]?.length ?? 0;
                if (k < searchLines.length - 1) {
                    matchEndIndex += 1; // Add newline character except for the last line
                }
            }
            yield content.substring(matchStartIndex, matchEndIndex);
        }
    }
};
export const BlockAnchorReplacer = function* (content, find) {
    const originalLines = content.split("\n");
    const searchLines = find.split("\n");
    if (searchLines.length < 3) {
        return;
    }
    if (searchLines[searchLines.length - 1] === "") {
        searchLines.pop();
    }
    const firstLineSearch = searchLines[0]?.trim() ?? "";
    const lastLineSearch = searchLines[searchLines.length - 1]?.trim() ?? "";
    const searchBlockSize = searchLines.length;
    // Collect all candidate positions where both anchors match
    const candidates = [];
    for (let i = 0; i < originalLines.length; i++) {
        if (originalLines[i]?.trim() !== firstLineSearch) {
            continue;
        }
        // Look for the matching last line after this first line
        for (let j = i + 2; j < originalLines.length; j++) {
            if (originalLines[j]?.trim() === lastLineSearch) {
                candidates.push({ startLine: i, endLine: j });
                break; // Only match the first occurrence of the last line
            }
        }
    }
    // Return immediately if no candidates
    if (candidates.length === 0) {
        return;
    }
    // Handle single candidate scenario (using relaxed threshold)
    if (candidates.length === 1) {
        const candidate = candidates[0];
        const { startLine, endLine } = candidate;
        const actualBlockSize = endLine - startLine + 1;
        let similarity = 0;
        const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2); // Middle lines only
        if (linesToCheck > 0) {
            for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
                const searchLineTrimmed = searchLines[j]?.trim() ?? "";
                const actualLineTrimmed = originalLines[startLine + j]?.trim() ?? "";
                const maxLen = Math.max(searchLineTrimmed.length, actualLineTrimmed.length);
                if (maxLen === 0) {
                    similarity += 1;
                }
                else {
                    const distance = levenshtein(searchLineTrimmed, actualLineTrimmed);
                    similarity += 1 - distance / maxLen;
                }
            }
            similarity = similarity / linesToCheck;
        }
        else {
            similarity = 1; // Only first and last lines, which already match
        }
        if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
            const blockLines = originalLines.slice(startLine, endLine + 1);
            yield blockLines.join("\n");
        }
        return;
    }
    // Handle multiple candidates scenario (using stricter threshold)
    for (const { startLine, endLine } of candidates) {
        const actualBlockSize = endLine - startLine + 1;
        let similarity = 0;
        const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2); // Middle lines only
        if (linesToCheck > 0) {
            for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
                const searchLineTrimmed = searchLines[j]?.trim() ?? "";
                const actualLineTrimmed = originalLines[startLine + j]?.trim() ?? "";
                const maxLen = Math.max(searchLineTrimmed.length, actualLineTrimmed.length);
                if (maxLen === 0) {
                    similarity += 1;
                }
                else {
                    const distance = levenshtein(searchLineTrimmed, actualLineTrimmed);
                    similarity += 1 - distance / maxLen;
                }
            }
            similarity = similarity / linesToCheck;
        }
        else {
            similarity = 1; // Only first and last lines, which already match
        }
        if (similarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD) {
            const blockLines = originalLines.slice(startLine, endLine + 1);
            yield blockLines.join("\n");
            return; // Return first good match
        }
    }
};
export const WhitespaceNormalizedReplacer = function* (content, find) {
    const normalizeWhitespace = (text) => {
        return text.replace(/\s+/g, " ").trim();
    };
    const normalizedFind = normalizeWhitespace(find);
    // Handle single line matches
    const lines = content.split("\n");
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        if (line && normalizeWhitespace(line) === normalizedFind) {
            yield line;
        }
        else if (line) {
            // Only check for substring matches if the full line doesn't match
            const normalizedLine = normalizeWhitespace(line);
            if (normalizedLine.includes(normalizedFind)) {
                // Find the actual substring in the original line that matches
                const words = find.trim().split(/\s+/);
                if (words.length > 0) {
                    const pattern = words
                        .map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
                        .join("\\s+");
                    try {
                        const regex = new RegExp(pattern);
                        const match = line.match(regex);
                        if (match?.[0]) {
                            yield match[0];
                        }
                    }
                    catch {
                        // Invalid regex pattern, skip
                    }
                }
            }
        }
    }
    // Handle multi-line matches
    const findLines = find.split("\n");
    if (findLines.length > 1) {
        for (let i = 0; i <= lines.length - findLines.length; i++) {
            const block = lines.slice(i, i + findLines.length);
            if (normalizeWhitespace(block.join("\n")) === normalizedFind) {
                yield block.join("\n");
            }
        }
    }
};
export const IndentationFlexibleReplacer = function* (content, find) {
    const removeIndentation = (text) => {
        const lines = text.split("\n");
        const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
        if (nonEmptyLines.length === 0)
            return text;
        const minIndent = Math.min(...nonEmptyLines.map((line) => {
            const match = line.match(/^(\s*)/);
            return match?.[1]?.length ?? 0;
        }));
        return lines
            .map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
            .join("\n");
    };
    const normalizedFind = removeIndentation(find);
    const contentLines = content.split("\n");
    const findLines = find.split("\n");
    for (let i = 0; i <= contentLines.length - findLines.length; i++) {
        const block = contentLines.slice(i, i + findLines.length).join("\n");
        if (removeIndentation(block) === normalizedFind) {
            yield block;
        }
    }
};
export const EscapeNormalizedReplacer = function* (content, find) {
    const unescapeString = (str) => {
        return str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
            switch (capturedChar) {
                case "n":
                    return "\n";
                case "t":
                    return "\t";
                case "r":
                    return "\r";
                case "'":
                    return "'";
                case '"':
                    return '"';
                case "`":
                    return "`";
                case "\\":
                    return "\\";
                case "\n":
                    return "\n";
                case "$":
                    return "$";
                default:
                    return match;
            }
        });
    };
    const unescapedFind = unescapeString(find);
    // Try direct match with unescaped find string
    if (content.includes(unescapedFind)) {
        yield unescapedFind;
    }
    // Also try finding escaped versions in content that match unescaped find
    const lines = content.split("\n");
    const findLines = unescapedFind.split("\n");
    for (let i = 0; i <= lines.length - findLines.length; i++) {
        const block = lines.slice(i, i + findLines.length).join("\n");
        const unescapedBlock = unescapeString(block);
        if (unescapedBlock === unescapedFind) {
            yield block;
        }
    }
};
function trimDiff(diff) {
    const lines = diff.split("\n");
    const contentLines = lines.filter((line) => (line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
        !line.startsWith("---") &&
        !line.startsWith("+++"));
    if (contentLines.length === 0)
        return diff;
    let min = Infinity;
    for (const line of contentLines) {
        const content = line.slice(1);
        if (content.trim().length > 0) {
            const match = content.match(/^(\s*)/);
            if (match?.[1])
                min = Math.min(min, match[1].length);
        }
    }
    if (min === Infinity || min === 0)
        return diff;
    const trimmedLines = lines.map((line) => {
        if ((line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
            !line.startsWith("---") &&
            !line.startsWith("+++")) {
            const prefix = line[0] ?? "";
            const content = line.slice(1);
            return prefix + content.slice(min);
        }
        return line;
    });
    return trimmedLines.join("\n");
}
export function replace(content, oldString, newString, replaceAll = false) {
    if (oldString === newString) {
        throw new ToolExecutionError("oldString and newString must be different", {
            toolName: "",
            input: {},
            context: {},
            severity: "warn",
        });
    }
    for (const replacer of [
        SimpleReplacer,
        LineTrimmedReplacer,
        BlockAnchorReplacer,
        WhitespaceNormalizedReplacer,
        IndentationFlexibleReplacer,
        EscapeNormalizedReplacer,
    ]) {
        for (const search of replacer(content, oldString)) {
            const index = content.indexOf(search);
            if (index === -1)
                continue;
            if (replaceAll) {
                return content.replaceAll(search, newString);
            }
            const lastIndex = content.lastIndexOf(search);
            if (index !== lastIndex) {
                continue;
            }
            return (content.substring(0, index) +
                newString +
                content.substring(index + search.length));
        }
    }
    throw new ToolExecutionError("oldString not found in content or was found multiple times", { toolName: "", input: {}, context: {}, severity: "warn" });
}
export const buildEditFileToolFactory = createToolFactory("build_editFile", ({ clark, services, }) => {
    return {
        category: ToolCategory.FILE,
        description: `
      Performs exact string replacements in files. Use this for making single, precise edits to existing files.

      Before using this tool:
      - You must use the Read tool to understand the file's contents first. This tool will error if you attempt an edit without reading the file.
      - ALWAYS prefer editing existing files over creating new ones. For new files, use the Write tool.

      Usage requirements:
      - When editing text from Read tool output, preserve exact indentation (tabs/spaces) as it appears AFTER the line number prefix
      - The line number prefix format is: spaces + line number + tab. Never include the line number prefix in oldString or newString
      - The edit will FAIL if oldString is not unique in the file. Use more surrounding context to make it unique, or use replaceAll
      - Use replaceAll for replacing and renaming strings across the entire file (useful for variable renaming)
      - Only use emojis if explicitly requested

      For multiple related edits to the same file, consider using the MultiEdit tool instead.

      DO NOT USE FOR API SOURCE CODE. Use the appropriate specialized tools instead.
    `,
        inputSchema: z.object({
            filePath: z
                .string()
                .describe("The relative path to the file to modify"),
            oldString: z.string().describe("The text to replace"),
            newString: z
                .string()
                .describe("The text to replace it with (must be different from oldString)"),
            replaceAll: z
                .boolean()
                .optional()
                .describe("Replace all occurrences of oldString (default false)"),
        }),
        execute: async ({ filePath, oldString, newString, replaceAll = false, }) => {
            if (!filePath) {
                throw new ToolExecutionError("filePath is required", {
                    toolName: "build_editFile",
                    input: {},
                    context: {},
                    severity: "warn",
                });
            }
            // Prevent editing API source code - use writeApi + buildApi instead
            // Match apis/*.ts but not apis/*.d.ts (type definitions are allowed)
            if (/^apis\/[^/]+\.ts$/.test(filePath) && !filePath.endsWith(".d.ts")) {
                throw new ToolExecutionError(`Cannot edit API source code directly. Use the writeApi tool to modify "${filePath}", then buildApi to compile and validate it.`, {
                    toolName: "build_editFile",
                    input: { filePath },
                    context: {},
                    severity: "warn",
                });
            }
            if (oldString === newString) {
                throw new ToolExecutionError("oldString and newString must be different", { toolName: "", input: {}, context: {}, severity: "warn" });
            }
            let diff = "";
            let contentOld = "";
            let contentNew = "";
            // Handle empty oldString as file creation
            if (oldString === "") {
                contentNew = newString;
                diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew));
                await services.appShell.writeFile(filePath, newString);
                // Immediately invalidate TypeScript cache since we just modified files
                debugCache.invalidateTypeScript();
                // Update last modification time
                clark.updateContext({
                    lastFileModificationTime: Date.now(),
                });
                // Trigger eager TypeScript check after file creation
                const typeCheckFunction = createTypeScriptCheckFunction(services.appShell, clark.context.abortController?.signal);
                debugCache.startEagerTypeScriptCheck(typeCheckFunction);
                return {
                    filePath,
                    diff,
                    message: `Successfully created file ${filePath}`,
                };
            }
            try {
                contentOld = await services.appShell.readFile(filePath);
            }
            catch {
                throw new ToolExecutionError(`File ${filePath} not found`, {
                    toolName: "",
                    input: {},
                    context: {},
                    severity: "warn",
                });
            }
            // Perform replacement
            try {
                contentNew = replace(contentOld, oldString, newString, replaceAll);
            }
            catch (error) {
                throw new Error(`Failed to replace text in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
            }
            // Generate diff
            diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew));
            await services.appShell.writeFile(filePath, contentNew);
            // Immediately invalidate TypeScript cache since we just modified files
            debugCache.invalidateTypeScript();
            // Update last modification time
            clark.updateContext({
                lastFileModificationTime: Date.now(),
            });
            // Trigger eager TypeScript check after file edit (debounced to avoid excessive checks)
            const typeCheckFunction = createTypeScriptCheckFunction(services.appShell, clark.context.abortController?.signal);
            debugCache.startEagerTypeScriptCheck(typeCheckFunction);
            const result = {
                filePath,
                diff,
                oldStringLength: oldString.length,
                newStringLength: newString.length,
                replaceAll,
                message: `Successfully ${replaceAll ? "replaced all occurrences" : "replaced"} text in ${filePath}`,
            };
            return result;
        },
        getSummary: (input, output) => {
            const action = input.replaceAll ? "Replaced all" : "Edited";
            const charInfo = "oldStringLength" in output && "newStringLength" in output
                ? ` (${output.oldStringLength}→${output.newStringLength} chars)`
                : "";
            return {
                output: `${action}${charInfo}`,
            };
        },
    };
});
//# sourceMappingURL=build-edit-file.js.map