import { diffLines, diffWords } from "diff";
/**
 * Compute a structured diff between two strings.
 *
 * @param oldText - the "before" text
 * @param newText - the "after" text
 * @param contextLines - number of unchanged lines to show around each change
 * @returns a DiffResult with hunks, additions, and deletions counts
 */
export function computeDiff(oldText, newText, contextLines = 3) {
    const allLines = buildDiffLines(oldText, newText);
    return groupIntoHunks(allLines, contextLines);
}
/**
 * Re-group previously computed flat lines into hunks.
 * Used when expanding collapsed sections without re-diffing.
 *
 * @param allLines - the full flat diff lines from a previous computation
 * @param contextLines - number of context lines around changes
 * @returns a DiffResult with new hunk groupings
 */
export function regroupLines(allLines, contextLines) {
    return groupIntoHunks(allLines, contextLines);
}
/**
 * Build paired rows for split (side-by-side) view from flat diff lines.
 * Context lines appear on both sides. Adjacent removed+added lines
 * are paired into the same row.
 *
 * @param lines - flat diff lines
 * @returns paired rows for split rendering
 */
export function buildSplitLines(lines) {
    const rows = [];
    let i = 0;
    while (i < lines.length) {
        const line = lines[i];
        if (line.type === 'context') {
            rows.push({ left: line, right: line });
            i++;
            continue;
        }
        i = collectAndPairChanges(lines, i, rows);
    }
    return rows;
}
/**
 * Collect consecutive removed then added lines starting at `index`,
 * pair them into split rows, and return the new index.
 * @param lines - flat diff lines
 * @param index - starting index
 * @param rows - output array to push paired rows into
 */
function collectAndPairChanges(lines, index, rows) {
    const removed = [];
    while (index < lines.length && lines[index].type === 'removed') {
        removed.push(lines[index]);
        index++;
    }
    const added = [];
    while (index < lines.length && lines[index].type === 'added') {
        added.push(lines[index]);
        index++;
    }
    const maxPairs = Math.max(removed.length, added.length);
    for (let j = 0; j < maxPairs; j++) {
        rows.push({
            left: j < removed.length ? removed[j] : undefined,
            right: j < added.length ? added[j] : undefined,
        });
    }
    return index;
}
/**
 * Normalize values for diffing. If `reformatJson` is true,
 * parse and re-stringify with sorted keys and consistent indentation.
 * @param value
 * @param reformatJson
 */
export function normalizeForDiff(value, reformatJson = false) {
    if (typeof value === 'object' && value !== null) {
        return JSON.stringify(sortKeysDeep(value), null, 4);
    }
    if (typeof value === 'string' && reformatJson) {
        try {
            const parsed = JSON.parse(value);
            return JSON.stringify(sortKeysDeep(parsed), null, 4);
        }
        catch (_a) {
            return value;
        }
    }
    return String(value !== null && value !== void 0 ? value : '');
}
function sortKeysDeep(obj) {
    if (Array.isArray(obj)) {
        return obj.map(sortKeysDeep);
    }
    if (obj !== null && typeof obj === 'object') {
        const sorted = {};
        const keys = Object.keys(obj).sort((a, b) => a.localeCompare(b));
        for (const key of keys) {
            sorted[key] = sortKeysDeep(obj[key]);
        }
        return sorted;
    }
    return obj;
}
/**
 * Build a flat list of DiffLines from two text strings.
 * @param oldText
 * @param newText
 */
function buildDiffLines(oldText, newText) {
    const changes = diffLines(oldText, newText);
    const lines = [];
    let oldLineNum = 1;
    let newLineNum = 1;
    for (const change of changes) {
        const changeLines = splitIntoLines(change.value);
        for (const line of changeLines) {
            if (change.added) {
                lines.push({
                    type: 'added',
                    content: line,
                    newLineNumber: newLineNum++,
                });
            }
            else if (change.removed) {
                lines.push({
                    type: 'removed',
                    content: line,
                    oldLineNumber: oldLineNum++,
                });
            }
            else {
                lines.push({
                    type: 'context',
                    content: line,
                    oldLineNumber: oldLineNum++,
                    newLineNumber: newLineNum++,
                });
            }
        }
    }
    addWordLevelHighlighting(lines);
    return lines;
}
/**
 * Split a string into lines, handling the trailing newline
 * that jsdiff includes in each change value.
 * @param text
 */
function splitIntoLines(text) {
    if (!text) {
        return [];
    }
    const lines = text.split('\n');
    // jsdiff includes a trailing newline, producing an empty last element
    if (lines.length > 0 && lines.at(-1) === '') {
        lines.pop();
    }
    return lines;
}
/**
 * Pair adjacent removed+added lines and compute word-level diffs
 * to highlight only the specific segments that changed.
 * @param lines
 */
function addWordLevelHighlighting(lines) {
    let i = 0;
    while (i < lines.length) {
        // Find consecutive removed lines
        const removedStart = i;
        while (i < lines.length && lines[i].type === 'removed') {
            i++;
        }
        const removedEnd = i;
        // Find consecutive added lines right after
        const addedStart = i;
        while (i < lines.length && lines[i].type === 'added') {
            i++;
        }
        const addedEnd = i;
        const removedCount = removedEnd - removedStart;
        const addedCount = addedEnd - addedStart;
        // Pair them up for word-level highlighting
        if (removedCount > 0 && addedCount > 0) {
            const pairCount = Math.min(removedCount, addedCount);
            for (let j = 0; j < pairCount; j++) {
                const removedLine = lines[removedStart + j];
                const addedLine = lines[addedStart + j];
                const [removedSegments, addedSegments] = computeWordSegments(removedLine.content, addedLine.content);
                removedLine.segments = removedSegments;
                addedLine.segments = addedSegments;
            }
        }
        // Skip context lines
        if (i === removedStart) {
            i++;
        }
    }
}
/**
 * Compute word-level diff segments for a pair of lines.
 * @param oldContent
 * @param newContent
 */
function computeWordSegments(oldContent, newContent) {
    const wordChanges = diffWords(oldContent, newContent);
    const removedSegments = [];
    const addedSegments = [];
    for (const change of wordChanges) {
        if (change.added) {
            addedSegments.push({ value: change.value, type: 'added' });
        }
        else if (change.removed) {
            removedSegments.push({ value: change.value, type: 'removed' });
        }
        else {
            removedSegments.push({ value: change.value, type: 'equal' });
            addedSegments.push({ value: change.value, type: 'equal' });
        }
    }
    return [removedSegments, addedSegments];
}
/**
 * Group a flat list of diff lines into hunks with context.
 * @param lines
 * @param contextLines
 */
function groupIntoHunks(lines, contextLines) {
    if (lines.length === 0) {
        return { hunks: [], additions: 0, deletions: 0, allLines: lines };
    }
    let additions = 0;
    let deletions = 0;
    for (const line of lines) {
        if (line.type === 'added') {
            additions++;
        }
        else if (line.type === 'removed') {
            deletions++;
        }
    }
    // If there are no changes, return a single empty result
    if (additions === 0 && deletions === 0) {
        return { hunks: [], additions: 0, deletions: 0, allLines: lines };
    }
    // Find ranges of changed lines with their context
    const changeIndices = [];
    for (const [i, line] of lines.entries()) {
        if (line.type !== 'context') {
            changeIndices.push(i);
        }
    }
    // Build hunk boundaries
    const hunkBoundaries = buildHunkBoundaries(changeIndices, lines.length, contextLines);
    const hunks = [];
    let prevEnd = 0;
    for (const boundary of hunkBoundaries) {
        const hunkLines = lines.slice(boundary.start, boundary.end);
        const hiddenBefore = boundary.start - prevEnd;
        const collapsedBefore = hiddenBefore > 0 ? hiddenBefore : undefined;
        hunks.push({
            lines: hunkLines,
            collapsedBefore,
            startIndex: boundary.start,
        });
        prevEnd = boundary.end;
    }
    // Calculate collapsed lines after the last hunk
    const lastBoundary = hunkBoundaries.at(-1);
    const collapsedAfter = lastBoundary.end < lines.length
        ? lines.length - lastBoundary.end
        : undefined;
    return { hunks, additions, deletions, collapsedAfter, allLines: lines };
}
/**
 * Build the start/end boundaries of each hunk based on change positions.
 * Merges hunks that overlap or are adjacent.
 * @param changeIndices
 * @param totalLines
 * @param contextLines
 */
function buildHunkBoundaries(changeIndices, totalLines, contextLines) {
    if (changeIndices.length === 0) {
        return [];
    }
    const boundaries = [];
    let currentStart = Math.max(0, changeIndices[0] - contextLines);
    let currentEnd = Math.min(totalLines, changeIndices[0] + contextLines + 1);
    for (let i = 1; i < changeIndices.length; i++) {
        const changeStart = Math.max(0, changeIndices[i] - contextLines);
        const changeEnd = Math.min(totalLines, changeIndices[i] + contextLines + 1);
        if (changeStart <= currentEnd) {
            // Merge overlapping hunks
            currentEnd = Math.max(currentEnd, changeEnd);
        }
        else {
            boundaries.push({ start: currentStart, end: currentEnd });
            currentStart = changeStart;
            currentEnd = changeEnd;
        }
    }
    boundaries.push({ start: currentStart, end: currentEnd });
    return boundaries;
}
