/**
 * Cell diff formatter — formats categorized dirty cells into a human-readable
 * summary for the LLM. Adapted from client/shared with simplified output.
 */
import { getColumnLetter, parseExcelAddress } from './address-utils.js';
import { IssueType, MUST_FIX_ISSUES } from './cell-categorizer.js';
import { shouldIncludeCell } from './types.js';
/** Maximum number of rows shown per section before truncation. */
const MAX_ROWS_TO_SHOW = 10;
/** Maximum sample cells shown per row before truncation. */
const MAX_SAMPLES_PER_ROW = 5;
/** Display symbol for undefined/empty cell values. */
const UNDEFINED_SYMBOL = '∅';
// =============================================================================
// Sampling
// =============================================================================
/** Pick first, last, and evenly-spaced items in between. Deterministic. */
function sampleWithFirstAndLast(items, maxSamples) {
    if (items.length <= maxSamples)
        return items;
    const selected = [0];
    const lastIndex = items.length - 1;
    if (maxSamples >= 2)
        selected.push(lastIndex);
    const remaining = maxSamples - 2;
    if (remaining > 0 && items.length > 2) {
        const step = (lastIndex - 1) / (remaining + 1);
        for (let i = 0; i < remaining; i++) {
            selected.push(Math.round(1 + step * (i + 1)));
        }
    }
    const unique = [...new Set(selected)].sort((a, b) => a - b);
    return unique.map((i) => items[i]);
}
// =============================================================================
// Value formatting
// =============================================================================
/** Stringify a raw cell value for display when no engine displayValue is available. */
function stringifyValue(value) {
    if (value === undefined || value === null)
        return UNDEFINED_SYMBOL;
    if (typeof value === 'string')
        return value.length === 0 ? UNDEFINED_SYMBOL : value;
    return String(value);
}
// TODO: computeBridge.getDisplayValue() returns number instead of string for
// plain value cells in headless/napi mode (contract says Promise<string>).
// We work around this by using cellData.formatted from getRange() in
// mutation-tracker.ts, but if getDisplayValue is ever used again, the kernel
// bug needs fixing. See: mog/kernel/src/bridges/compute/compute-bridge.gen.ts
function getDisplayValue(cell) {
    if (cell.displayValue !== undefined && cell.displayValue !== null) {
        return cell.displayValue.length === 0 ? UNDEFINED_SYMBOL : cell.displayValue;
    }
    return stringifyValue(cell.value);
}
function getOldDisplayValue(cell) {
    if (cell.oldDisplayValue !== undefined && cell.oldDisplayValue !== null) {
        return cell.oldDisplayValue.length === 0 ? UNDEFINED_SYMBOL : cell.oldDisplayValue;
    }
    return stringifyValue(cell.oldValue);
}
/** Group cells by sheet+row and sort by sheet name then row index. */
function collectSortedRows(cells) {
    const bySheetAndRow = new Map();
    for (const cell of cells) {
        if (!shouldIncludeCell(cell))
            continue;
        try {
            const parsed = parseExcelAddress(cell.address);
            const sheet = cell.sheet || 'Sheet1';
            let rowsMap = bySheetAndRow.get(sheet);
            if (!rowsMap) {
                rowsMap = new Map();
                bySheetAndRow.set(sheet, rowsMap);
            }
            let rowCells = rowsMap.get(parsed.row);
            if (!rowCells) {
                rowCells = [];
                rowsMap.set(parsed.row, rowCells);
            }
            rowCells.push({ cell, col: parsed.col });
        }
        catch {
            continue;
        }
    }
    // Sort cells within each row by column
    for (const [, rowsMap] of bySheetAndRow) {
        for (const [, rowCells] of rowsMap) {
            rowCells.sort((a, b) => a.col - b.col);
        }
    }
    const allRows = [];
    for (const [sheet, rowsMap] of bySheetAndRow) {
        for (const [row, parsedCells] of rowsMap) {
            allRows.push({ sheet, row, parsedCells });
        }
    }
    allRows.sort((a, b) => {
        const s = a.sheet.localeCompare(b.sheet);
        return s !== 0 ? s : a.row - b.row;
    });
    return allRows;
}
// =============================================================================
// Row formatting
// =============================================================================
/** Format a single row's cell changes as indented lines with old -> new values. */
function formatRow(sheet, row, parsedCells, indent = 4) {
    const lines = [];
    const pad = ' '.repeat(indent);
    const samplePad = ' '.repeat(indent + 2);
    const minCol = parsedCells[0].col;
    const maxCol = parsedCells[parsedCells.length - 1].col;
    const colRange = minCol === maxCol
        ? getColumnLetter(minCol)
        : `${getColumnLetter(minCol)}-${getColumnLetter(maxCol)}`;
    lines.push(`${pad}${sheet}!Row ${row + 1} (${colRange}): ${parsedCells.length} cells`);
    const samples = sampleWithFirstAndLast(parsedCells, MAX_SAMPLES_PER_ROW);
    for (const { cell } of samples) {
        const oldStr = getOldDisplayValue(cell);
        const newStr = getDisplayValue(cell);
        const formulaSuffix = cell.formula ? ` [${cell.formula}]` : '';
        lines.push(`${samplePad}→ ${cell.address}: ${oldStr} -> ${newStr}${formulaSuffix}`);
    }
    if (parsedCells.length > MAX_SAMPLES_PER_ROW) {
        lines.push(`${samplePad}... and ${parsedCells.length - MAX_SAMPLES_PER_ROW} more cells`);
    }
    return lines;
}
/** Format a sampled subset of rows, with a trailing "... and N more rows" if truncated. */
function formatRows(rows, indent = 4) {
    const lines = [];
    const selected = sampleWithFirstAndLast(rows, MAX_ROWS_TO_SHOW);
    for (const { sheet, row, parsedCells } of selected) {
        lines.push(...formatRow(sheet, row, parsedCells, indent));
    }
    if (rows.length > MAX_ROWS_TO_SHOW) {
        lines.push(`${' '.repeat(indent)}... and ${rows.length - MAX_ROWS_TO_SHOW} more rows`);
    }
    return lines;
}
// =============================================================================
// Section formatters
// =============================================================================
/** Render a titled section (e.g. "Changed without issues: 42 total cells") with sampled rows. */
function formatSection(cells, title, mustFix = false, indent = 2) {
    const filtered = cells.filter(shouldIncludeCell);
    if (filtered.length === 0)
        return '';
    const prefix = mustFix ? 'MUST FIX: ' : '';
    const lines = [];
    lines.push(`${' '.repeat(indent)}${prefix}${title}: ${filtered.length} total cells`);
    const rows = collectSortedRows(filtered);
    lines.push(...formatRows(rows, indent + 2));
    return lines.join('\n');
}
function formatGoodSection(goodCells, indent = 2) {
    return formatSection(goodCells, 'Changed without issues', false, indent);
}
function formatProblemSections(categorized, indent = 2) {
    if (categorized.allProblemCells.length === 0)
        return '';
    const sections = [];
    const categories = [
        { cells: categorized.largePercentages, type: IssueType.LARGE_PERCENTAGE },
        { cells: categorized.hardcodedNumbers, type: IssueType.UNTAGGED_HARDCODED_NUMBER },
        { cells: categorized.hardcodedNumbersInFormulas, type: IssueType.HARDCODED_NUMBER_IN_FORMULA },
        { cells: categorized.invalidFormulas, type: IssueType.INVALID_FORMULA }
    ];
    for (const { cells, type } of categories) {
        if (cells.length === 0)
            continue;
        const dirtyCells = cells.map((c) => c.cell);
        const mustFix = MUST_FIX_ISSUES.has(type);
        const section = formatSection(dirtyCells, type, mustFix, indent);
        if (section)
            sections.push(section);
    }
    if (sections.length === 0)
        return '';
    return `${' '.repeat(indent)}Cells that need review:\n${sections.join('\n\n')}`;
}
// =============================================================================
// Main entry
// =============================================================================
/**
 * Produce the full "CELL DIFF SUMMARY" text from categorized cells.
 *
 * Includes sections for clean changes and problem cells (hardcodes, formula errors, etc.),
 * each with sampled row details showing old -> new values.
 */
export function formatCellDiff(categorizedCells, stringOnlyCount = 0) {
    const allCells = categorizedCells.goodCells.length + categorizedCells.allProblemCells.length;
    if (allCells === 0 && stringOnlyCount === 0)
        return 'No changes made.';
    const sections = [];
    sections.push('--- CELL DIFF SUMMARY ---');
    sections.push(`(Formatted display values shown. ${UNDEFINED_SYMBOL} = undefined/empty.)`);
    const goodSection = formatGoodSection(categorizedCells.goodCells);
    if (goodSection) {
        sections.push('');
        sections.push(goodSection);
    }
    const problemSection = formatProblemSections(categorizedCells);
    if (problemSection) {
        sections.push('');
        sections.push(problemSection);
    }
    if (stringOnlyCount > 0) {
        sections.push('');
        sections.push(`  ${stringOnlyCount} text-only cell(s) not shown.`);
    }
    return sections.join('\n');
}
//# sourceMappingURL=cell-diff-formatter.js.map