import { readFileSync, writeFileSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { relative } from 'node:path';
import { logger } from '../../utils/logger.js';
import { FileUndoManager } from '../../state/file-undo.js';
import { FileStateCache, unifiedDiff } from '../../state/file-cache.js';
import { resolveAndValidatePath } from '../../utils/path-safety.js';
import { editSchema } from './schema.js';
import { AutoHealer } from '../shared/auto-heal.js';
import { ImportGraph } from '../../intelligence/index.js';
import { buildFailedEditResult, classifyEditFailure, computeAstEdit, computeAstPatternEdit, computeSingleEdit, } from './match.js';
import { executeNotebookEdit } from './notebook.js';
import { summarizeError } from '../../utils/error-display.js';
import { toRecord } from '../../utils/record-coerce.js';
import { runValidators as runSharedValidators, formatValidatorFailure, } from '../shared/validators.js';
const DIFF_TRUNCATE_THRESHOLD = 5000;
const DIFF_PREVIEW_LENGTH = 500;
async function runValidators(validators, cwd) {
    const failures = await runSharedValidators(validators, cwd);
    return failures[0] ?? null;
}
function prepareTextEditInput(input, env, transactionMode) {
    const resolvedPaths = new Map();
    for (const item of input.edits) {
        if (resolvedPaths.has(item.path))
            continue;
        try {
            resolvedPaths.set(item.path, resolveAndValidatePath(item.path, env.cwd));
        }
        catch (err) {
            const msg = summarizeError(err);
            if (transactionMode === 'atomic') {
                return { error: `Path error for '${item.path}': ${msg}` };
            }
        }
    }
    const uniquePaths = new Set((input.edits ?? []).map((e) => resolvedPaths.get(e.path) ?? e.path));
    const fileContents = new Map();
    const fileReadErrors = new Map();
    for (const resolvedPath of uniquePaths) {
        const cacheResult = env.fileCache.lookup(resolvedPath);
        if (cacheResult.status === 'modified') {
            const msg = `OCC conflict: '${resolvedPath}' was modified externally since last read`;
            if (transactionMode === 'atomic') {
                return { error: msg };
            }
            fileReadErrors.set(resolvedPath, msg);
            continue;
        }
        try {
            const content = readFileSync(resolvedPath, 'utf-8');
            fileContents.set(resolvedPath, content);
        }
        catch {
            const msg = `File not found or unreadable: '${resolvedPath}'`;
            if (transactionMode === 'atomic') {
                return { error: msg };
            }
            fileReadErrors.set(resolvedPath, msg);
        }
    }
    return {
        resolvedPaths,
        fileContents,
        fileReadErrors,
        workingContents: new Map(fileContents),
    };
}
async function writeSuccessfulTextEdits(results, resolvedPaths, workingContents, fileContents, env, writtenPaths) {
    for (const r of results) {
        if (!r.success)
            continue;
        const resolvedPath = resolvedPaths.get(r.path);
        if (!resolvedPath || writtenPaths.has(resolvedPath))
            continue;
        const newContent = workingContents.get(resolvedPath);
        if (newContent === undefined)
            continue;
        try {
            await writeFile(resolvedPath, newContent, 'utf-8');
            env.fileCache.update(resolvedPath, newContent);
            writtenPaths.add(resolvedPath);
            if (env.fileUndoManager) {
                try {
                    const originalContent = fileContents.get(resolvedPath) ?? null;
                    env.fileUndoManager.snapshot({
                        path: resolvedPath,
                        beforeContent: originalContent,
                        afterContent: newContent,
                        tool: 'edit',
                    });
                }
                catch {
                    // Undo snapshot failure should not fail the edit.
                }
            }
            env.changeTracker?.recordChange(resolvedPath);
        }
        catch (err) {
            const msg = `Write failed for '${resolvedPath}': ${summarizeError(err)}`;
            for (const res of results) {
                if (res.path === r.path) {
                    res.success = false;
                    res.error = msg;
                }
            }
        }
    }
}
async function buildImportGraphWarning(cwd, writtenPaths) {
    try {
        const graph = new ImportGraph();
        graph.markDirty();
        await graph.build(cwd);
        const editedAbsPaths = [...writtenPaths];
        const affectedSet = new Set();
        for (const edited of editedAbsPaths) {
            for (const dep of graph.findTransitiveDependents(edited)) {
                affectedSet.add(dep);
            }
        }
        for (const edited of editedAbsPaths) {
            affectedSet.delete(edited);
        }
        if (affectedSet.size === 0)
            return undefined;
        const affectedList = Array.from(affectedSet);
        const proc = Bun.spawn(['npx', 'tsc', '--noEmit', ...affectedList], {
            cwd,
            stdout: 'pipe',
            stderr: 'pipe',
        });
        const [exitCode, stdoutText, stderrText] = await Promise.all([
            proc.exited,
            new Response(proc.stdout).text(),
            new Response(proc.stderr).text(),
        ]);
        if (exitCode === 0)
            return undefined;
        const relAffected = affectedList.map((f) => relative(cwd, f));
        const outputLines = (stderrText + '\n' + stdoutText)
            .split('\n')
            .filter((line) => relAffected.some((rel) => line.includes(rel)));
        if (outputLines.length > 0) {
            return `\n⚠ Import graph: ${affectedSet.size} transitive dependent(s) affected by this edit — type errors detected in downstream files:\n${outputLines.join('\n')}`;
        }
        return `\n⚠ Import graph: ${affectedSet.size} transitive dependent(s) affected. tsc reported errors outside the affected set — check unrelated files.`;
    }
    catch (err) {
        logger.warn('[import-graph] Import graph tracing failed', { error: summarizeError(err) });
        return undefined;
    }
}
async function restoreOriginalContents(fileContents, env) {
    for (const [resolvedPath, originalContent] of fileContents) {
        try {
            await writeFile(resolvedPath, originalContent, 'utf-8');
            env.fileCache.update(resolvedPath, originalContent);
        }
        catch {
            // Continue restoring the remaining files.
        }
    }
}
async function repairAfterValidationFailure(fileContents, workingContents, failureMessages, env) {
    let healed = false;
    for (const [resolvedPath, originalContent] of fileContents) {
        const newContent = workingContents.get(resolvedPath);
        if (newContent === undefined || newContent === originalContent)
            continue;
        const healResult = env.configManager && env.toolLLM
            ? await new AutoHealer(env.configManager, env.toolLLM).heal(resolvedPath, newContent, failureMessages)
            : { healed: false, content: newContent };
        if (healResult.healed) {
            try {
                writeFileSync(resolvedPath, healResult.content, 'utf-8');
                env.fileCache.update(resolvedPath, healResult.content);
                healed = true;
            }
            catch {
                // Leave validation failure handling to the caller if the heal write fails.
            }
        }
    }
    return { healed };
}
async function validateAfterTextEdits(validators, cwd, transactionMode, fileContents, workingContents, env) {
    const failure = await runValidators(validators, cwd);
    if (!failure)
        return {};
    const failureMessages = [formatValidatorFailure(failure)];
    const repair = await repairAfterValidationFailure(fileContents, workingContents, failureMessages, env);
    if (repair.healed) {
        const healFailure = await runValidators(validators, cwd);
        if (!healFailure) {
            return {};
        }
    }
    if (transactionMode === 'atomic') {
        await restoreOriginalContents(fileContents, env);
    }
    return {
        error: `Post-edit validation failed${transactionMode === 'atomic' ? ' — edits rolled back' : ''}. ${formatValidatorFailure(failure)}`,
    };
}
function formatOutput(results, format, dryRun) {
    const totalApplied = results.filter((r) => r.success).length;
    const totalFailed = results.filter((r) => !r.success).length;
    const dryTag = dryRun ? ' (dry run)' : '';
    if (format === 'count_only') {
        return JSON.stringify({ applied: totalApplied, failed: totalFailed, dry_run: dryRun });
    }
    const lines = [];
    lines.push(`Edits applied: ${totalApplied}, failed: ${totalFailed}${dryTag}`);
    if (format === 'minimal') {
        for (const r of results) {
            if (r.success) {
                const id = r.id ? ` [${r.id}]` : '';
                const statusTag = r.status ? ` [${r.status}]` : '';
                lines.push(`  OK${statusTag}${id}: ${r.path} (${r.occurrencesReplaced} replacement(s))`);
                if (r.warning) {
                    lines.push(`    WARN: ${r.warning}`);
                }
            }
            else {
                const id = r.id ? ` [${r.id}]` : '';
                const statusTag = r.status ? ` [${r.status}]` : '';
                lines.push(`  FAIL${statusTag}${id}: ${r.path} — ${r.error}`);
                if (r.hint) {
                    lines.push(`    HINT: ${r.hint}`);
                }
            }
        }
        return lines.join('\n');
    }
    for (const r of results) {
        const id = r.id ? ` [${r.id}]` : '';
        if (r.success) {
            const statusTag = r.status ? ` [${r.status}]` : '';
            lines.push(`\n--- ${r.path}${id}${statusTag} (${r.occurrencesReplaced} replacement(s))${dryTag} ---`);
            if (r.diff) {
                if (r.diff_truncated) {
                    lines.push(`[diff truncated — showing first ${DIFF_PREVIEW_LENGTH} chars]`);
                    lines.push(r.diff_preview ?? r.diff.slice(0, DIFF_PREVIEW_LENGTH));
                }
                else {
                    lines.push(r.diff);
                }
            }
            if (r.warning) {
                lines.push(`  WARN: ${r.warning}`);
            }
        }
        else {
            const statusTag = r.status ? ` [${r.status}]` : '';
            lines.push(`\n--- ${r.path}${id}${statusTag} FAILED ---`);
            lines.push(`  Error: ${r.error}`);
            if (r.hint) {
                lines.push(`  Hint: ${r.hint}`);
            }
        }
    }
    return lines.join('\n');
}
async function executeTextEdits(input, env) {
    const matchMode = input.match?.mode ?? 'exact';
    const caseSensitive = input.match?.case_sensitive ?? true;
    const whitespaceSensitive = input.match?.whitespace_sensitive ?? true;
    const multiline = input.match?.multiline ?? false;
    const transactionMode = input.transaction?.mode ?? 'atomic';
    const outputFormat = input.output?.format ?? 'minimal';
    const diffContext = input.output?.diff_context ?? 3;
    const dryRun = input.dry_run ?? false;
    const validateBefore = input.validate?.before ?? [];
    const validateAfter = input.validate?.after ?? [];
    const cwd = env.cwd;
    if (!dryRun && validateBefore.length > 0) {
        const failure = await runValidators(validateBefore, cwd);
        if (failure) {
            return { success: false, error: `Pre-edit validation failed. ${formatValidatorFailure(failure)}` };
        }
    }
    const prepResult = prepareTextEditInput(input, env, transactionMode);
    if ('error' in prepResult) {
        return { success: false, error: prepResult.error };
    }
    const { resolvedPaths, fileContents, fileReadErrors, workingContents } = prepResult;
    const results = [];
    let atomicFailed = false;
    let atomicFailError = '';
    for (const item of input.edits) {
        const resolvedPath = resolvedPaths.get(item.path);
        if (!resolvedPath) {
            results.push(buildFailedEditResult(item, `Path resolution failed for '${item.path}'`, 'failed'));
            if (transactionMode === 'atomic') {
                atomicFailed = true;
                atomicFailError = `Path resolution failed for '${item.path}'`;
                break;
            }
            continue;
        }
        if (fileReadErrors.has(resolvedPath)) {
            const readErrMsg = fileReadErrors.get(resolvedPath);
            results.push(buildFailedEditResult(item, readErrMsg, classifyEditFailure(readErrMsg)));
            if (transactionMode === 'atomic') {
                atomicFailed = true;
                atomicFailError = readErrMsg;
                break;
            }
            continue;
        }
        const currentContent = workingContents.get(resolvedPath);
        if (currentContent === undefined) {
            results.push(buildFailedEditResult(item, `No content available for '${resolvedPath}'`, 'failed'));
            if (transactionMode === 'atomic') {
                atomicFailed = true;
                atomicFailError = `No content available for '${resolvedPath}'`;
                break;
            }
            continue;
        }
        let editResult;
        if (matchMode === 'ast_pattern') {
            editResult = await computeAstPatternEdit(currentContent, item, resolvedPath);
        }
        else if (matchMode === 'ast') {
            editResult = await computeAstEdit(currentContent, item, resolvedPath);
        }
        else {
            editResult = computeSingleEdit(currentContent, item, matchMode, caseSensitive, whitespaceSensitive, multiline);
        }
        if ('error' in editResult) {
            const errMsg = editResult.error;
            results.push({
                ...buildFailedEditResult(item, errMsg, classifyEditFailure(errMsg)),
                hint: 'hint' in editResult ? editResult.hint : undefined,
            });
            if (transactionMode === 'atomic') {
                atomicFailed = true;
                atomicFailError = errMsg;
                break;
            }
            continue;
        }
        const oldContent = currentContent;
        workingContents.set(resolvedPath, editResult.newContent);
        let diff;
        let diffTruncated;
        let diffPreview;
        if (outputFormat === 'with_diff' || outputFormat === 'verbose' || dryRun) {
            const rawDiff = unifiedDiff(oldContent, editResult.newContent, resolvedPath, diffContext);
            if (rawDiff.length > DIFF_TRUNCATE_THRESHOLD) {
                diffTruncated = true;
                diffPreview = rawDiff.slice(0, DIFF_PREVIEW_LENGTH);
                diff = diffPreview;
            }
            else {
                diff = rawDiff;
            }
        }
        results.push({
            id: item.id,
            path: item.path,
            success: true,
            status: 'applied',
            occurrencesReplaced: editResult.occurrencesReplaced,
            diff,
            diff_truncated: diffTruncated,
            diff_preview: diffPreview,
            warning: editResult.warning,
        });
    }
    if (transactionMode === 'atomic' && atomicFailed) {
        const atomicResults = (input.edits ?? []).map((item, idx) => {
            const r = results[idx];
            if (r && !r.success)
                return r;
            return {
                id: item.id,
                path: item.path,
                success: false,
                status: 'failed',
                error: r?.success ? 'Rolled back due to atomic transaction failure' : (r?.error ?? atomicFailError),
            };
        });
        return {
            success: false,
            error: `Atomic transaction failed: ${atomicFailError}`,
            output: formatOutput(atomicResults, outputFormat, dryRun),
        };
    }
    const writtenPaths = new Set();
    if (!dryRun) {
        await writeSuccessfulTextEdits(results, resolvedPaths, workingContents, fileContents, env, writtenPaths);
    }
    const anySuccess = results.some((r) => r.success);
    let importGraphWarning;
    if (!dryRun && anySuccess) {
        importGraphWarning = await buildImportGraphWarning(cwd, writtenPaths);
    }
    if (!dryRun && anySuccess && validateAfter.length > 0) {
        const validationResult = await validateAfterTextEdits(validateAfter, cwd, transactionMode, fileContents, workingContents, env);
        if (validationResult.error) {
            return { success: false, error: validationResult.error };
        }
    }
    return {
        success: anySuccess,
        output: formatOutput(results, outputFormat, dryRun) + (importGraphWarning ?? ''),
    };
}
function resolveEditCwd(options) {
    if (options?.cwd && options.cwd.trim().length > 0) {
        return options.cwd;
    }
    const workingDirectory = options?.configManager?.getWorkingDirectory();
    if (workingDirectory && workingDirectory.trim().length > 0) {
        return workingDirectory;
    }
    throw new Error('createEditTool requires an explicit cwd or configManager.getWorkingDirectory()');
}
export function createEditTool(fileCache, options) {
    const cwd = resolveEditCwd(options);
    const definition = {
        name: 'edit',
        description: 'Edit files by finding and replacing text. Supports exact, fuzzy, and regex matching. ' +
            'Handles multiple edits in one call with atomic or partial transaction semantics. ' +
            'Detects OCC conflicts when files have been modified externally. ' +
            'Also supports Jupyter notebook (.ipynb) cell operations via notebook_operations field.',
        parameters: toRecord(editSchema),
        sideEffects: ['write_fs'],
        concurrency: 'serial',
        supportsProgress: true,
    };
    async function execute(args) {
        try {
            const input = args;
            if (!input.edits && !input.notebook_operations) {
                return { success: false, error: 'Either edits or notebook_operations must be provided' };
            }
            if (input.edits && input.notebook_operations) {
                return { success: false, error: 'Provide either edits or notebook_operations, not both' };
            }
            const env = {
                fileCache,
                cwd,
                fileUndoManager: options?.fileUndoManager,
                configManager: options?.configManager,
                toolLLM: options?.toolLLM,
                changeTracker: options?.changeTracker,
            };
            if (input.notebook_operations) {
                return await executeNotebookEdit(input, env);
            }
            return await executeTextEdits(input, env);
        }
        catch (err) {
            return { success: false, error: `Unexpected error: ${summarizeError(err)}` };
        }
    }
    return { definition, execute };
}
