// engine/spec-diff-engine.ts — SPEC-362: Visual Spec Diff & 3-way Merge
// ---------------------------------------------------------------------------
// LCS-based line diff
// ---------------------------------------------------------------------------
/**
 * Compute Longest Common Subsequence between two arrays of strings.
 * Returns the LCS table for backtracking.
 */
function computeLCS(a, b) {
    const m = a.length;
    const n = b.length;
    const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            const prevRow = dp[i - 1];
            const curRow = dp[i];
            const aEl = a[i - 1];
            const bEl = b[j - 1];
            if (prevRow === undefined || curRow === undefined) {
                continue;
            }
            if (aEl === bEl) {
                curRow[j] = (prevRow[j - 1] ?? 0) + 1;
            }
            else {
                curRow[j] = Math.max(prevRow[j] ?? 0, curRow[j - 1] ?? 0);
            }
        }
    }
    return dp;
}
/**
 * Backtrack through LCS table to produce diff lines.
 */
function backtrackDiff(dp, a, b, i, j, lines) {
    if (i === 0 && j === 0) {
        return;
    }
    if (i === 0) {
        backtrackDiff(dp, a, b, i, j - 1, lines);
        const content = b[j - 1] ?? '';
        lines.push({ type: 'added', content, lineNumberB: j });
    }
    else if (j === 0) {
        backtrackDiff(dp, a, b, i - 1, j, lines);
        const content = a[i - 1] ?? '';
        lines.push({ type: 'removed', content, lineNumberA: i });
    }
    else if (a[i - 1] === b[j - 1]) {
        backtrackDiff(dp, a, b, i - 1, j - 1, lines);
        const content = a[i - 1] ?? '';
        lines.push({ type: 'unchanged', content, lineNumberA: i, lineNumberB: j });
    }
    else {
        const prevRowAtJ = dp[i - 1]?.[j] ?? 0;
        const curRowAtJMinus1 = dp[i]?.[j - 1] ?? 0;
        if (prevRowAtJ >= curRowAtJMinus1) {
            backtrackDiff(dp, a, b, i - 1, j, lines);
            const content = a[i - 1] ?? '';
            lines.push({ type: 'removed', content, lineNumberA: i });
        }
        else {
            backtrackDiff(dp, a, b, i, j - 1, lines);
            const content = b[j - 1] ?? '';
            lines.push({ type: 'added', content, lineNumberB: j });
        }
    }
}
export function computeLineDiff(textA, textB) {
    const linesA = textA.split('\n');
    const linesB = textB.split('\n');
    // For large files, fall back to a simpler approach to avoid stack overflow
    if (linesA.length > 500 || linesB.length > 500) {
        return computeLineDiffIterative(linesA, linesB);
    }
    const dp = computeLCS(linesA, linesB);
    const diffLines = [];
    backtrackDiff(dp, linesA, linesB, linesA.length, linesB.length, diffLines);
    return diffLines;
}
/**
 * Iterative diff for large files (avoids recursion limit).
 */
function computeLineDiffIterative(linesA, linesB) {
    const result = [];
    const setA = new Set(linesA);
    const setB = new Set(linesB);
    let ia = 0;
    let ib = 0;
    while (ia < linesA.length || ib < linesB.length) {
        const lineA = linesA[ia];
        const lineB = linesB[ib];
        if (ia >= linesA.length || lineB === undefined) {
            result.push({ type: 'added', content: lineB ?? '', lineNumberB: ib + 1 });
            ib++;
        }
        else if (ib >= linesB.length || lineA === undefined) {
            result.push({ type: 'removed', content: lineA ?? '', lineNumberA: ia + 1 });
            ia++;
        }
        else if (lineA === lineB) {
            result.push({ type: 'unchanged', content: lineA, lineNumberA: ia + 1, lineNumberB: ib + 1 });
            ia++;
            ib++;
        }
        else if (!setB.has(lineA)) {
            result.push({ type: 'removed', content: lineA, lineNumberA: ia + 1 });
            ia++;
        }
        else if (!setA.has(lineB)) {
            result.push({ type: 'added', content: lineB, lineNumberB: ib + 1 });
            ib++;
        }
        else {
            result.push({ type: 'removed', content: lineA, lineNumberA: ia + 1 });
            result.push({ type: 'added', content: lineB, lineNumberB: ib + 1 });
            ia++;
            ib++;
        }
    }
    return result;
}
// ---------------------------------------------------------------------------
// Visual diff output
// ---------------------------------------------------------------------------
const TYPE_COLORS = {
    added: '#d4edda',
    removed: '#f8d7da',
    unchanged: '#ffffff',
    context: '#fff3cd',
};
const TYPE_PREFIXES = {
    added: '+',
    removed: '-',
    unchanged: ' ',
    context: '~',
};
export function computeVisualDiff(textA, textB, labelA = 'A', labelB = 'B') {
    const diffLines = computeLineDiff(textA, textB);
    const added = diffLines.filter((l) => l.type === 'added').length;
    const removed = diffLines.filter((l) => l.type === 'removed').length;
    const unchanged = diffLines.filter((l) => l.type === 'unchanged').length;
    const htmlOutput = buildHtmlDiff(diffLines, labelA, labelB);
    return {
        linesA: textA.split('\n').length,
        linesB: textB.split('\n').length,
        added,
        removed,
        unchanged,
        diffLines,
        htmlOutput,
    };
}
function escapeHtml(text) {
    return text
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;');
}
function buildHtmlDiff(diffLines, labelA, labelB) {
    const rows = diffLines
        .map((line) => {
        const bg = TYPE_COLORS[line.type];
        const prefix = TYPE_PREFIXES[line.type];
        const lineA = line.lineNumberA !== undefined ? String(line.lineNumberA) : '';
        const lineB = line.lineNumberB !== undefined ? String(line.lineNumberB) : '';
        const content = escapeHtml(line.content);
        return (`<tr style="background:${bg}">` +
            `<td style="color:#888;padding:2px 8px;user-select:none;font-size:12px">${lineA}</td>` +
            `<td style="color:#888;padding:2px 8px;user-select:none;font-size:12px">${lineB}</td>` +
            `<td style="padding:2px 4px;font-family:monospace;color:#666">${prefix}</td>` +
            `<td style="padding:2px 8px;font-family:monospace;white-space:pre">${content}</td>` +
            `</tr>`);
    })
        .join('\n');
    return (`<!DOCTYPE html><html><head><meta charset="utf-8">` +
        `<title>Spec Diff: ${escapeHtml(labelA)} vs ${escapeHtml(labelB)}</title></head>` +
        `<body style="font-family:sans-serif;margin:0;padding:16px">` +
        `<h2>Diff: <code>${escapeHtml(labelA)}</code> vs <code>${escapeHtml(labelB)}</code></h2>` +
        `<table style="border-collapse:collapse;width:100%;font-size:14px">` +
        `<thead><tr style="background:#f0f0f0">` +
        `<th style="padding:4px 8px">#A</th><th style="padding:4px 8px">#B</th>` +
        `<th style="padding:4px 8px"></th><th style="padding:4px 8px;text-align:left">Content</th>` +
        `</tr></thead><tbody>\n${rows}\n</tbody></table>` +
        `</body></html>`);
}
export function formatDiffText(diffLines, labelA = 'A', labelB = 'B') {
    const header = `--- ${labelA}\n+++ ${labelB}\n`;
    const body = diffLines.map((l) => `${TYPE_PREFIXES[l.type]} ${l.content}`).join('\n');
    return header + body;
}
// ---------------------------------------------------------------------------
// 3-way merge
// ---------------------------------------------------------------------------
export function threeWayMerge(base, textA, textB, labelA = 'A', labelB = 'B') {
    const aLines = textA.split('\n');
    const bLines = textB.split('\n');
    const mergedLines = [];
    let conflictCount = 0;
    let ia = 0;
    let ib = 0;
    void base; // base used as reference — iterating A and B directly
    while (ia < aLines.length || ib < bLines.length) {
        const lineA = aLines[ia];
        const lineB = bLines[ib];
        if (ia >= aLines.length || lineA === undefined) {
            mergedLines.push({ type: 'b', content: lineB ?? '' });
            ib++;
        }
        else if (ib >= bLines.length || lineB === undefined) {
            mergedLines.push({ type: 'a', content: lineA });
            ia++;
        }
        else if (lineA === lineB) {
            mergedLines.push({ type: 'base', content: lineA });
            ia++;
            ib++;
        }
        else {
            // Conflict
            conflictCount++;
            mergedLines.push({ type: 'conflict-start', content: `<<<<<<< ${labelA}` });
            mergedLines.push({ type: 'a', content: lineA });
            mergedLines.push({ type: 'conflict-sep', content: '=======' });
            mergedLines.push({ type: 'b', content: lineB });
            mergedLines.push({ type: 'conflict-end', content: `>>>>>>> ${labelB}` });
            ia++;
            ib++;
        }
    }
    const mergedText = mergedLines.map((l) => l.content).join('\n');
    return {
        hasConflicts: conflictCount > 0,
        conflictCount,
        mergedLines,
        mergedText,
    };
}
// ---------------------------------------------------------------------------
// Smart merge conflict resolver
// ---------------------------------------------------------------------------
export function smartMergeConflictResolver(mergedText) {
    const lines = mergedText.split('\n');
    const result = [];
    let resolvedCount = 0;
    let remainingConflicts = 0;
    let i = 0;
    while (i < lines.length) {
        const line = lines[i] ?? '';
        if (!line.startsWith('<<<<<<<')) {
            result.push(line);
            i++;
            continue;
        }
        // Find conflict boundaries
        const startIdx = i;
        let sepIdx = -1;
        let endIdx = -1;
        for (let j = i + 1; j < lines.length; j++) {
            const jLine = lines[j] ?? '';
            if (jLine.startsWith('=======')) {
                sepIdx = j;
            }
            if (jLine.startsWith('>>>>>>>')) {
                endIdx = j;
                break;
            }
        }
        if (sepIdx === -1 || endIdx === -1) {
            // Malformed conflict marker — keep as-is
            result.push(line);
            i++;
            continue;
        }
        const aLines = lines.slice(startIdx + 1, sepIdx);
        const bLines = lines.slice(sepIdx + 1, endIdx);
        // Resolution 1: Both sides identical
        if (aLines.join('\n') === bLines.join('\n')) {
            result.push(...aLines);
            resolvedCount++;
            i = endIdx + 1;
            continue;
        }
        // Resolution 2: Whitespace-only diff (trim each line)
        const aNorm = aLines.map((l) => l.trim()).join('\n');
        const bNorm = bLines.map((l) => l.trim()).join('\n');
        if (aNorm === bNorm) {
            result.push(...bLines); // prefer b (more recent)
            resolvedCount++;
            i = endIdx + 1;
            continue;
        }
        // Cannot auto-resolve
        remainingConflicts++;
        for (let j = startIdx; j <= endIdx; j++) {
            result.push(lines[j] ?? '');
        }
        i = endIdx + 1;
    }
    return {
        resolvedCount,
        remainingConflicts,
        mergedText: result.join('\n'),
    };
}
// ---------------------------------------------------------------------------
// Merge preview
// ---------------------------------------------------------------------------
export function buildMergePreview(base, textA, textB) {
    const mergeResult = threeWayMerge(base, textA, textB);
    const canAutoMerge = !mergeResult.hasConflicts;
    return {
        textA,
        textB,
        base,
        mergeResult,
        canAutoMerge,
    };
}
//# sourceMappingURL=spec-diff-engine.js.map