/**
 * Mutation Tracker — Proxy-based dirty cell tracking for sheet-exec.
 *
 * Wraps Workbook/Worksheet objects in Proxies that record every mutation
 * (setCell, setRange, setCells, etc.). Captures old values before
 * each mutation. After code execution, resolves mutations to CellEdit[]
 * by batch-fetching current values and display values.
 *
 * Each MutationRecord stores a workbook reference so resolve() works
 * correctly across multiple workbooks.
 *
 * Proxy pattern adapted from mog/runtime/headless-server/src/execution/executor.ts.
 *
 * TODO: Sparse range optimization — resolve() fetches the full bounding box for
 * all mutations on a sheet. If mutations are sparse (e.g. A1 and Z100), this
 * fetches many unneeded cells. Consider partitioning into sub-ranges.
 */
import { indexToAddress, parseExcelAddress } from './cell-diff/address-utils.js';
// =============================================================================
// Capture helpers
// =============================================================================
/** Capture old value + display value for a single cell before mutation. */
async function captureOld(ws, address) {
    let oldValue = null;
    let oldDisplayValue;
    try {
        oldValue = await ws.getValue(address);
        oldDisplayValue = await ws.getDisplayValue(address);
    }
    catch {
        /* cell may not exist yet */
    }
    return { oldValue, oldDisplayValue };
}
/**
 * Batch-capture old values for a rectangular range.
 * Uses a single getRange call instead of N individual getValue calls.
 */
async function captureOldRange(ws, startRow, startCol, endRow, endCol) {
    const result = new Map();
    try {
        const rangeData = await ws.getRange(startRow, startCol, endRow, endCol);
        for (let r = startRow; r <= endRow; r++) {
            for (let c = startCol; c <= endCol; c++) {
                const addr = indexToAddress(r, c);
                const cellData = rangeData[r - startRow]?.[c - startCol];
                let oldDisplayValue;
                try {
                    oldDisplayValue = await ws.getDisplayValue(addr);
                }
                catch {
                    /* cell may not exist yet */
                }
                result.set(addr, {
                    oldValue: cellData?.value ?? null,
                    oldDisplayValue
                });
            }
        }
    }
    catch {
        // Range may not exist yet — return empty map, individual cells will get null
    }
    return result;
}
// =============================================================================
// Proxy wrappers
// =============================================================================
/**
 * Wrap a Worksheet in a Proxy that intercepts mutation methods
 * (setCell, setRange, setCells, setDateValue, setTimeValue) and records
 * old values before delegating to the real method.
 */
function createTrackedWorksheet(ws, wb, mutations) {
    return new Proxy(ws, {
        get(target, prop, receiver) {
            const original = Reflect.get(target, prop, receiver);
            // setCell(address, value) or setCell(row, col, value)
            if (prop === 'setCell') {
                return async (first, ...rest) => {
                    const addr = typeof first === 'string' ? first : indexToAddress(first, rest[0]);
                    const old = await captureOld(target, addr);
                    mutations.push({ workbook: wb, worksheet: target, address: addr, ...old });
                    return original.call(target, first, ...rest);
                };
            }
            // setRange(range, values[][]) or setRange(startRow, startCol, values[][])
            if (prop === 'setRange') {
                return async (first, ...rest) => {
                    if (typeof first === 'string') {
                        const parts = first.split(':');
                        const start = parseExcelAddress(parts[0]);
                        const end = parts[1] ? parseExcelAddress(parts[1]) : start;
                        const oldValues = await captureOldRange(target, start.row, start.col, end.row, end.col);
                        for (let r = start.row; r <= end.row; r++) {
                            for (let c = start.col; c <= end.col; c++) {
                                const addr = indexToAddress(r, c);
                                const old = oldValues.get(addr) ?? { oldValue: null };
                                mutations.push({ workbook: wb, worksheet: target, address: addr, ...old });
                            }
                        }
                        return original.call(target, first, ...rest);
                    }
                    else {
                        const startRow = first;
                        const startCol = rest[0];
                        const values = rest[1];
                        if (Array.isArray(values)) {
                            const endRow = startRow + values.length - 1;
                            const endCol = startCol + Math.max(...values.map((r) => r?.length ?? 0)) - 1;
                            const oldValues = await captureOldRange(target, startRow, startCol, endRow, endCol);
                            for (let r = 0; r < values.length; r++) {
                                for (let c = 0; c < (values[r]?.length ?? 0); c++) {
                                    const addr = indexToAddress(startRow + r, startCol + c);
                                    const old = oldValues.get(addr) ?? { oldValue: null };
                                    mutations.push({ workbook: wb, worksheet: target, address: addr, ...old });
                                }
                            }
                        }
                        return original.call(target, first, ...rest);
                    }
                };
            }
            // setCells([{ addr, value }])
            if (prop === 'setCells') {
                return async (updates) => {
                    if (updates.length > 0) {
                        const positions = updates.map((u) => parseExcelAddress(u.addr));
                        const rows = positions.map((p) => p.row);
                        const cols = positions.map((p) => p.col);
                        const oldValues = await captureOldRange(target, Math.min(...rows), Math.min(...cols), Math.max(...rows), Math.max(...cols));
                        for (const u of updates) {
                            const old = oldValues.get(u.addr) ?? { oldValue: null };
                            mutations.push({ workbook: wb, worksheet: target, address: u.addr, ...old });
                        }
                    }
                    return original.call(target, updates);
                };
            }
            // setArrayFormula({ startRow, startCol, endRow, endCol }, formula)
            if (prop === 'setArrayFormula') {
                return async (range, formula) => {
                    const oldValues = await captureOldRange(target, range.startRow, range.startCol, range.endRow, range.endCol);
                    for (let r = range.startRow; r <= range.endRow; r++) {
                        for (let c = range.startCol; c <= range.endCol; c++) {
                            const addr = indexToAddress(r, c);
                            const old = oldValues.get(addr) ?? { oldValue: null };
                            mutations.push({ workbook: wb, worksheet: target, address: addr, ...old });
                        }
                    }
                    return original.call(target, range, formula);
                };
            }
            // setDateValue(row, col, date)
            if (prop === 'setDateValue') {
                return async (row, col, ...args) => {
                    const addr = indexToAddress(row, col);
                    const old = await captureOld(target, addr);
                    mutations.push({ workbook: wb, worksheet: target, address: addr, ...old });
                    return original.call(target, row, col, ...args);
                };
            }
            // setTimeValue(row, col, date)
            if (prop === 'setTimeValue') {
                return async (row, col, ...args) => {
                    const addr = indexToAddress(row, col);
                    const old = await captureOld(target, addr);
                    mutations.push({ workbook: wb, worksheet: target, address: addr, ...old });
                    return original.call(target, row, col, ...args);
                };
            }
            if (typeof original === 'function') {
                return original.bind(target);
            }
            return original;
        }
    });
}
/**
 * Wrap a Workbook in a Proxy that returns tracked worksheets from all
 * sheet-access methods (getSheetByIndex, getSheetByName, addSheet, etc.).
 */
function createTrackedWorkbook(wb, mutations) {
    return new Proxy(wb, {
        get(target, prop, receiver) {
            const original = Reflect.get(target, prop, receiver);
            if (prop === 'getSheetByIndex') {
                return async (index) => {
                    const ws = await original.call(target, index);
                    return createTrackedWorksheet(ws, target, mutations);
                };
            }
            if (prop === 'getSheet') {
                return async (name) => {
                    const ws = await original.call(target, name);
                    return createTrackedWorksheet(ws, target, mutations);
                };
            }
            if (prop === 'findSheet') {
                return async (name) => {
                    const ws = await original.call(target, name);
                    if (!ws)
                        return null;
                    return createTrackedWorksheet(ws, target, mutations);
                };
            }
            if (prop === 'activeSheet') {
                const ws = original;
                return createTrackedWorksheet(ws, target, mutations);
            }
            if (prop === 'addSheet') {
                return async (name, index) => {
                    const ws = await original.call(target, name, index);
                    return createTrackedWorksheet(ws, target, mutations);
                };
            }
            // Intercept sheets property to wrap sheets.add() return value
            if (prop === 'sheets') {
                const sheetsObj = original;
                return new Proxy(sheetsObj, {
                    get(sTarget, sProp, sReceiver) {
                        const sOriginal = Reflect.get(sTarget, sProp, sReceiver);
                        if (sProp === 'add') {
                            return async (name, index) => {
                                const ws = await sOriginal.call(sTarget, name, index);
                                return createTrackedWorksheet(ws, target, mutations);
                            };
                        }
                        if (typeof sOriginal === 'function')
                            return sOriginal.bind(sTarget);
                        return sOriginal;
                    }
                });
            }
            if (typeof original === 'function') {
                return original.bind(target);
            }
            return original;
        }
    });
}
// =============================================================================
// MutationTracker
// =============================================================================
export class MutationTracker {
    mutations = [];
    trackedWorkbooks = new Map();
    /**
     * Wrap a Workbook in tracking Proxies. Returns a cached proxy if this
     * workbook has already been wrapped, avoiding nested proxy chains.
     */
    track(wb) {
        const existing = this.trackedWorkbooks.get(wb);
        if (existing)
            return existing;
        const tracked = createTrackedWorkbook(wb, this.mutations);
        this.trackedWorkbooks.set(wb, tracked);
        return tracked;
    }
    /**
     * After execution, resolve all mutations to CellEdit[] by batch-fetching
     * current values, display values, and number formats from workbooks.
     *
     * Each mutation stores its workbook reference, so multi-workbook scenarios
     * resolve correctly.
     *
     * getRange returns CellData[][] where CellData = { value, formula?, format?, ... }
     * and format?.numberFormat holds the number format string.
     */
    async resolve() {
        if (this.mutations.length === 0)
            return [];
        const cellEdits = [];
        // Deduplicate: same worksheet+address → keep first (true pre-execution oldValue)
        const seen = new Map();
        for (const m of this.mutations) {
            const wsId = this.getWorksheetId(m.worksheet);
            const key = `${wsId}\0${m.address}`;
            if (!seen.has(key)) {
                seen.set(key, m);
            }
        }
        const unique = [...seen.values()];
        // Group by worksheet for batch fetching
        const groups = new Map();
        for (const m of unique) {
            const wsId = this.getWorksheetId(m.worksheet);
            let group = groups.get(wsId);
            if (!group) {
                group = { ws: m.worksheet, mutations: [] };
                groups.set(wsId, group);
            }
            group.mutations.push(m);
        }
        for (const [, { ws, mutations: sheetMutations }] of groups) {
            try {
                const resolvedSheetName = await ws.getName();
                // Parse addresses to row/col and compute bounding box
                const positions = sheetMutations.map((m) => {
                    const pos = parseExcelAddress(m.address);
                    return { ...pos, address: m.address, mutation: m };
                });
                const minRow = Math.min(...positions.map((p) => p.row));
                const maxRow = Math.max(...positions.map((p) => p.row));
                const minCol = Math.min(...positions.map((p) => p.col));
                const maxCol = Math.max(...positions.map((p) => p.col));
                // Single batch fetch — getRange(startRow, startCol, endRow, endCol) inclusive
                const rangeData = await ws.getRange(minRow, minCol, maxRow, maxCol);
                for (const pos of positions) {
                    const rowIdx = pos.row - minRow;
                    const colIdx = pos.col - minCol;
                    const cellData = rangeData[rowIdx]?.[colIdx];
                    // Prefer cellData.formatted (from queryRange) — it's the Rust-formatted
                    // display string and works reliably for formula cells. getDisplayValue()
                    // returns "" for formula cells in headless/napi mode.
                    const displayValue = cellData?.formatted ?? undefined;
                    cellEdits.push({
                        sheet: resolvedSheetName,
                        address: pos.address,
                        oldValue: pos.mutation.oldValue,
                        value: cellData?.value ?? null,
                        oldDisplayValue: pos.mutation.oldDisplayValue,
                        displayValue,
                        formula: cellData?.formula ?? null,
                        numberFormat: cellData?.format?.numberFormat
                    });
                }
            }
            catch {
                // Sheet may have been deleted — skip
            }
        }
        return cellEdits;
    }
    /** Stable identity for a worksheet across lookups. */
    worksheetIds = new Map();
    nextWsId = 0;
    getWorksheetId(ws) {
        let id = this.worksheetIds.get(ws);
        if (id === undefined) {
            id = this.nextWsId++;
            this.worksheetIds.set(ws, id);
        }
        return String(id);
    }
}
//# sourceMappingURL=mutation-tracker.js.map