import { readFileSync, writeFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
import { isNotebookFile } from '../../utils/notebook.js';
import { unifiedDiff, FileStateCache } from '../../state/file-cache.js';
import { FileUndoManager } from '../../state/file-undo.js';
import { logger } from '../../utils/logger.js';
import { resolveAndValidatePath } from '../../utils/path-safety.js';
import { summarizeError } from '../../utils/error-display.js';
export function normalizeSource(source) {
    if (Array.isArray(source))
        return source;
    const lines = source.split('\n');
    return lines.map((line, i) => (i < lines.length - 1 ? line + '\n' : line));
}
export function validateNotebook(parsed) {
    if (typeof parsed !== 'object' || parsed === null)
        return false;
    const nb = parsed;
    if (typeof nb['nbformat'] !== 'number')
        return false;
    if (!Array.isArray(nb['cells']))
        return false;
    for (const cell of nb['cells']) {
        if (!cell || typeof cell !== 'object')
            return false;
        const c = cell;
        if (!c['cell_type'] || typeof c['cell_type'] !== 'string')
            return false;
        if (c['source'] === undefined)
            return false;
    }
    return true;
}
export function resolveCellId(cells, cellId) {
    for (let i = 0; i < cells.length; i++) {
        const cell = cells[i];
        if (cell.id === cellId)
            return i;
        if (cell.metadata && cell.metadata['id'] === cellId)
            return i;
    }
    return -1;
}
export function generateCellId(existingCells) {
    const existingIds = new Set(existingCells?.map((c) => c.id).filter(Boolean) ?? []);
    let id;
    do {
        id = randomUUID().replace(/-/g, '').slice(0, 8);
    } while (existingIds.has(id));
    return id;
}
export function readNotebookFile(resolvedPath, fileCache) {
    const cacheResult = fileCache.lookup(resolvedPath);
    if (cacheResult.status === 'modified') {
        return { error: `OCC conflict: '${resolvedPath}' was modified externally since last read` };
    }
    let rawContent;
    try {
        rawContent = readFileSync(resolvedPath, 'utf-8');
    }
    catch {
        return { error: `File not found or unreadable: '${resolvedPath}'` };
    }
    try {
        const parsed = JSON.parse(rawContent);
        if (!validateNotebook(parsed)) {
            return { error: 'Not a valid Jupyter notebook: missing nbformat or cells array' };
        }
        return { notebook: parsed, rawContent };
    }
    catch (err) {
        return { error: `Failed to parse notebook JSON: ${summarizeError(err)}` };
    }
}
export function applyNotebookOperations(notebook, operations) {
    const needsCellIds = notebook.nbformat > 4 ||
        (notebook.nbformat === 4 && (notebook.nbformat_minor ?? 0) >= 5);
    let indexOffset = 0;
    let applied = 0;
    const summaryLines = [];
    for (const op of operations) {
        if (op.op === 'replace') {
            let idx;
            if (op.cell_id !== undefined) {
                idx = resolveCellId(notebook.cells, op.cell_id);
                if (idx === -1) {
                    return { success: false, applied, summary: summaryLines.join('\n'), error: `replace: cell_id '${op.cell_id}' not found` };
                }
            }
            else if (op.cell !== undefined) {
                idx = op.cell + indexOffset;
                if (idx < 0 || idx >= notebook.cells.length) {
                    return { success: false, applied, summary: summaryLines.join('\n'), error: `replace: cell index ${op.cell} out of range (notebook has ${notebook.cells.length} cells)` };
                }
            }
            else {
                return { success: false, applied, summary: summaryLines.join('\n'), error: 'replace: requires cell or cell_id' };
            }
            if (op.source === undefined) {
                return { success: false, applied, summary: summaryLines.join('\n'), error: 'replace: source is required' };
            }
            const cell = notebook.cells[idx];
            cell.source = normalizeSource(op.source);
            if (op.cell_type !== undefined && op.cell_type !== cell.cell_type) {
                cell.cell_type = op.cell_type;
                if (op.cell_type === 'code') {
                    if (cell.execution_count === undefined)
                        cell.execution_count = null;
                    if (cell.outputs === undefined)
                        cell.outputs = [];
                }
                else {
                    delete cell.execution_count;
                    delete cell.outputs;
                }
            }
            if (op.clear_outputs && cell.cell_type === 'code') {
                cell.outputs = [];
                cell.execution_count = null;
            }
            summaryLines.push(`  OK: replace cell[${idx}]`);
            applied++;
        }
        else if (op.op === 'insert') {
            if (op.source === undefined) {
                return { success: false, applied, summary: summaryLines.join('\n'), error: 'insert: source is required' };
            }
            if (op.cell_type === undefined) {
                return { success: false, applied, summary: summaryLines.join('\n'), error: 'insert: cell_type is required' };
            }
            const newCell = {
                cell_type: op.cell_type,
                source: normalizeSource(op.source),
                metadata: {},
            };
            if (op.cell_type === 'code') {
                newCell.execution_count = null;
                newCell.outputs = [];
            }
            if (needsCellIds) {
                newCell.id = generateCellId(notebook.cells);
            }
            let insertAt;
            if (op.cell_id !== undefined) {
                const refIdx = resolveCellId(notebook.cells, op.cell_id);
                if (refIdx === -1) {
                    return { success: false, applied, summary: summaryLines.join('\n'), error: `insert: cell_id '${op.cell_id}' not found` };
                }
                insertAt = refIdx + 1;
            }
            else if (op.after !== undefined) {
                if (op.after === -1) {
                    insertAt = 0;
                }
                else {
                    const adjustedAfter = op.after + indexOffset;
                    if (adjustedAfter < -1 || adjustedAfter >= notebook.cells.length) {
                        return { success: false, applied, summary: summaryLines.join('\n'), error: `insert: after index ${op.after} out of bounds (-1 to ${notebook.cells.length - 1})` };
                    }
                    insertAt = adjustedAfter + 1;
                }
            }
            else {
                insertAt = notebook.cells.length;
            }
            notebook.cells.splice(insertAt, 0, newCell);
            indexOffset++;
            summaryLines.push(`  OK: insert cell at[${insertAt}]`);
            applied++;
        }
        else if (op.op === 'delete') {
            let idx;
            if (op.cell_id !== undefined) {
                idx = resolveCellId(notebook.cells, op.cell_id);
                if (idx === -1) {
                    return { success: false, applied, summary: summaryLines.join('\n'), error: `delete: cell_id '${op.cell_id}' not found` };
                }
            }
            else if (op.cell !== undefined) {
                idx = op.cell + indexOffset;
                if (idx < 0 || idx >= notebook.cells.length) {
                    return { success: false, applied, summary: summaryLines.join('\n'), error: `delete: cell index ${op.cell} out of range (notebook has ${notebook.cells.length} cells)` };
                }
            }
            else {
                return { success: false, applied, summary: summaryLines.join('\n'), error: 'delete: requires cell or cell_id' };
            }
            notebook.cells.splice(idx, 1);
            indexOffset--;
            summaryLines.push(`  OK: delete cell[${idx}]`);
            applied++;
        }
    }
    return { success: true, applied, summary: summaryLines.join('\n') };
}
export function formatNotebookOutput(opsResult, outputFormat, dryRun, rawContent, newContent, resolvedPath, diffContext) {
    if (outputFormat === 'count_only') {
        return JSON.stringify({ applied: opsResult.applied, failed: 0, dry_run: dryRun });
    }
    if (outputFormat === 'minimal') {
        return `Notebook operations applied: ${opsResult.applied}, failed: 0${dryRun ? ' (dry run)' : ''}\n${opsResult.summary}`;
    }
    const diff = unifiedDiff(rawContent, newContent, resolvedPath, diffContext);
    return `Notebook operations applied: ${opsResult.applied}, failed: 0${dryRun ? ' (dry run)' : ''}\n${opsResult.summary}\n${diff}`;
}
export function executeNotebookEdit(input, env) {
    const nbOps = input.notebook_operations;
    const outputFormat = input.output?.format ?? 'minimal';
    const diffContext = input.output?.diff_context ?? 3;
    const dryRun = input.dry_run ?? false;
    if (!nbOps.path || typeof nbOps.path !== 'string') {
        return Promise.resolve({ success: false, error: 'notebook_operations.path is required and must be a string' });
    }
    if (!Array.isArray(nbOps.operations)) {
        return Promise.resolve({ success: false, error: 'notebook_operations.operations must be an array' });
    }
    let resolvedPath;
    try {
        resolvedPath = resolveAndValidatePath(nbOps.path, env.cwd);
    }
    catch (err) {
        return Promise.resolve({ success: false, error: `Path error: ${summarizeError(err)}` });
    }
    if (!isNotebookFile(resolvedPath)) {
        return Promise.resolve({ success: false, error: `notebook_operations requires a .ipynb file, got: ${nbOps.path}` });
    }
    const notebookRead = readNotebookFile(resolvedPath, env.fileCache);
    if ('error' in notebookRead) {
        return Promise.resolve({ success: false, error: notebookRead.error });
    }
    const notebook = notebookRead.notebook;
    const rawContent = notebookRead.rawContent;
    const opsResult = applyNotebookOperations(notebook, nbOps.operations);
    if (!opsResult.success) {
        return Promise.resolve({ success: false, error: opsResult.error ?? '' });
    }
    const newContent = JSON.stringify(notebook, null, 1) + '\n';
    if (dryRun) {
        return Promise.resolve({ success: true, output: formatNotebookOutput(opsResult, outputFormat, true, rawContent, newContent, resolvedPath, diffContext) });
    }
    try {
        writeFileSync(resolvedPath, newContent, 'utf-8');
    }
    catch (err) {
        return Promise.resolve({ success: false, error: `Write failed for '${resolvedPath}': ${summarizeError(err)}` });
    }
    env.fileCache.update(resolvedPath, newContent);
    if (env.fileUndoManager) {
        try {
            env.fileUndoManager.snapshot({
                path: resolvedPath,
                beforeContent: rawContent,
                afterContent: newContent,
                tool: 'edit',
            });
        }
        catch {
            // Undo snapshot failure should not fail the edit.
        }
    }
    return Promise.resolve({
        success: true,
        output: formatNotebookOutput(opsResult, outputFormat, false, rawContent, newContent, resolvedPath, diffContext),
    });
}
