/**
 * engine/living-spec/conflict-resolver.ts — SPEC-172: Merge conflict resolution
 *
 * Pure functions for detecting and resolving conflicts between spec changes and
 * code changes that occur simultaneously. Supports configurable resolution policies
 * and generates git-style conflict markers for unresolved conflicts.
 */
import { createHash } from 'node:crypto';
import { computeLineDiff } from '../shared/line-diff.js';
// ---------------------------------------------------------------------------
// Diff utilities
// ---------------------------------------------------------------------------
/**
 * Compute a simple line-level diff between two spec strings.
 * Delegates to the shared line-diff utility and adds change detection.
 */
export function computeSpecDiff(originalSpec, newSpec) {
    const { added, removed } = computeLineDiff(originalSpec, newSpec);
    const changed = [];
    // Detect changed lines: lines whose prefix (first word) matches but content differs
    for (const addedLine of added) {
        const prefix = addedLine.split(' ')[0] ?? '';
        if (prefix.length > 3) {
            const matchingRemoved = removed.find((r) => r.startsWith(prefix));
            if (matchingRemoved !== undefined) {
                changed.push(`${matchingRemoved} → ${addedLine}`);
            }
        }
    }
    return { added, removed, changed };
}
/**
 * Build a CodeDiff from a list of changed file paths.
 * File paths prefixed with '+' are added, '-' removed, otherwise modified.
 */
export function buildCodeDiff(codeChanges) {
    const addedFiles = [];
    const removedFiles = [];
    const modifiedFiles = [];
    const diffLines = [...codeChanges];
    for (const change of codeChanges) {
        if (change.startsWith('+')) {
            addedFiles.push(change.slice(1).trim());
        }
        else if (change.startsWith('-')) {
            removedFiles.push(change.slice(1).trim());
        }
        else {
            modifiedFiles.push(change.trim());
        }
    }
    return { addedFiles, removedFiles, modifiedFiles, diffLines };
}
// ---------------------------------------------------------------------------
// Conflict categorization
// ---------------------------------------------------------------------------
/**
 * Categorize a conflict item as semantic, structural, or cosmetic.
 *
 * - semantic: spec acceptance criteria directly contradict code behavior
 * - structural: additions/removals that create structural mismatches
 * - cosmetic: naming differences, whitespace, or formatting changes only
 */
export function categorizeConflict(conflict) {
    const specLower = conflict.specSide.toLowerCase();
    const codeLower = conflict.codeSide.toLowerCase();
    // Structural: one side has addition/removal the other lacks
    const structuralKeywords = ['added', 'removed', 'deleted', 'new function', 'missing'];
    for (const kw of structuralKeywords) {
        if (codeLower.includes(kw) || specLower.includes(kw)) {
            return 'structural';
        }
    }
    // Cosmetic: only naming/casing differences
    const specNormalized = specLower.replace(/[\s_-]/g, '');
    const codeNormalized = codeLower.replace(/[\s_-]/g, '');
    if (specNormalized === codeNormalized) {
        return 'cosmetic';
    }
    // Default: semantic
    return 'semantic';
}
// ---------------------------------------------------------------------------
// Conflict detection
// ---------------------------------------------------------------------------
/**
 * Detect conflicts between a spec diff and a code diff.
 * Returns conflict items representing mismatches that need resolution.
 */
export function detectConflicts(specDiff, codeDiff) {
    const conflicts = [];
    const now = new Date().toISOString();
    // Structural: spec adds ACs but code removes files
    if (specDiff.added.length > 0 && codeDiff.removedFiles.length > 0) {
        for (const addedLine of specDiff.added) {
            if (isAcceptanceCriterion(addedLine)) {
                for (const removedFile of codeDiff.removedFiles) {
                    const id = generateConflictId(addedLine, removedFile);
                    conflicts.push({
                        id,
                        specId: '',
                        description: `Spec adds AC but code removed file: ${removedFile}`,
                        specSide: addedLine,
                        codeSide: `removed file: ${removedFile}`,
                        category: 'structural',
                        detectedAt: now,
                    });
                }
            }
        }
    }
    // Structural: spec removes ACs but code adds files implementing them
    if (specDiff.removed.length > 0 && codeDiff.addedFiles.length > 0) {
        for (const removedLine of specDiff.removed) {
            if (isAcceptanceCriterion(removedLine)) {
                for (const addedFile of codeDiff.addedFiles) {
                    const id = generateConflictId(removedLine, addedFile);
                    conflicts.push({
                        id,
                        specId: '',
                        description: `Spec removes AC but code added file: ${addedFile}`,
                        specSide: `removed: ${removedLine}`,
                        codeSide: `added file: ${addedFile}`,
                        category: 'structural',
                        detectedAt: now,
                    });
                }
            }
        }
    }
    // Semantic: spec changed existing ACs while code also modified files
    if (specDiff.changed.length > 0 && codeDiff.modifiedFiles.length > 0) {
        for (const changedLine of specDiff.changed) {
            const id = generateConflictId(changedLine, codeDiff.modifiedFiles.join(','));
            conflicts.push({
                id,
                specId: '',
                description: `Spec AC changed while code was also modified`,
                specSide: changedLine,
                codeSide: `modified files: ${codeDiff.modifiedFiles.join(', ')}`,
                category: 'semantic',
                detectedAt: now,
            });
        }
    }
    // Cosmetic: spec has renamed items that differ only in casing/format from code
    for (const addedLine of specDiff.added) {
        for (const modifiedFile of codeDiff.modifiedFiles) {
            const baseName = modifiedFile
                .replace(/\.[^.]+$/, '')
                .split('/')
                .pop() ?? '';
            if (isCosmeticMismatch(addedLine, baseName)) {
                const id = generateConflictId(addedLine, modifiedFile);
                conflicts.push({
                    id,
                    specId: '',
                    description: `Cosmetic naming mismatch between spec and code`,
                    specSide: addedLine,
                    codeSide: modifiedFile,
                    category: 'cosmetic',
                    detectedAt: now,
                });
            }
        }
    }
    return conflicts;
}
// ---------------------------------------------------------------------------
// Auto-resolution
// ---------------------------------------------------------------------------
/**
 * Attempt to auto-resolve a single conflict using the given policy.
 * Returns null if the policy requires human review (always-ask).
 */
export function autoResolve(conflict, policy) {
    if (policy === 'always-ask') {
        return null;
    }
    const now = new Date().toISOString();
    if (policy === 'code-wins') {
        // Cosmetic conflicts are always safe to auto-resolve with code-wins
        // Structural and semantic: code wins for implementation details
        if (conflict.category === 'semantic' && !isImplementationDetail(conflict)) {
            return null; // Semantic AC conflicts need human review even with code-wins
        }
        return {
            conflict,
            appliedPolicy: policy,
            winner: 'code',
            explanation: `code-wins policy: code state accepted for ${conflict.category} conflict`,
            resolvedAt: now,
        };
    }
    if (policy === 'spec-wins') {
        // Spec wins for acceptance criteria by definition
        if (conflict.category === 'structural' && conflict.codeSide.startsWith('removed file')) {
            return null; // Cannot auto-resolve structural removal without human verification
        }
        return {
            conflict,
            appliedPolicy: policy,
            winner: 'spec',
            explanation: `spec-wins policy: spec state accepted for ${conflict.category} conflict`,
            resolvedAt: now,
        };
    }
    // policy === 'most-recent': cosmetic conflicts resolve to spec; others need human review
    if (conflict.category === 'cosmetic') {
        return {
            conflict,
            appliedPolicy: policy,
            winner: 'spec',
            explanation: `most-recent policy: spec is treated as most recent for cosmetic conflicts`,
            resolvedAt: now,
        };
    }
    // For semantic/structural, most-recent cannot determine winner without timestamps
    return null;
}
// ---------------------------------------------------------------------------
// Main orchestrator
// ---------------------------------------------------------------------------
/**
 * Resolve spec conflicts between an original spec, a new spec version, and code changes.
 * Returns a MergeConflictResolution with auto-resolved conflicts and those pending human review.
 */
export function resolveSpecConflict(originalSpec, newSpec, codeChanges, specId, policy = 'always-ask') {
    const specDiff = computeSpecDiff(originalSpec, newSpec);
    const codeDiff = buildCodeDiff(codeChanges);
    const rawConflicts = detectConflicts(specDiff, codeDiff);
    // Attach specId to all conflicts
    const conflicts = rawConflicts.map((c) => ({ ...c, specId }));
    const autoResolved = [];
    const pendingReview = [];
    for (const conflict of conflicts) {
        const resolution = autoResolve(conflict, policy);
        if (resolution !== null) {
            autoResolved.push(resolution);
        }
        else {
            pendingReview.push(conflict);
        }
    }
    const conflictMarkers = generateConflictMarkers(pendingReview, originalSpec, newSpec);
    return {
        specId,
        autoResolved,
        pendingReview,
        conflictMarkers,
        resolvedAt: new Date().toISOString(),
    };
}
// ---------------------------------------------------------------------------
// Conflict marker generation
// ---------------------------------------------------------------------------
/**
 * Generate git-style conflict markers for unresolved conflicts.
 */
export function generateConflictMarkers(pendingConflicts, originalSpec, newSpec) {
    if (pendingConflicts.length === 0) {
        return [];
    }
    const markers = [];
    for (const conflict of pendingConflicts) {
        markers.push(`<<<< SPEC (${conflict.id})`, conflict.specSide, `====`, conflict.codeSide, `>>>> CODE`, `# Conflict: ${conflict.description} (category: ${conflict.category})`, '');
    }
    // Suppress unused variable warnings — params are available for future context use
    void originalSpec;
    void newSpec;
    return markers;
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
function generateConflictId(a, b) {
    // Content-based hash: same inputs always produce the same conflict ID,
    // enabling idempotent conflict detection across multiple runs.
    return createHash('sha256').update(`${a}:${b}`).digest('hex').slice(0, 12);
}
function isAcceptanceCriterion(line) {
    const lower = line.toLowerCase();
    return (lower.includes('- [ ]') ||
        lower.includes('- [x]') ||
        lower.includes('ac-') ||
        lower.startsWith('acceptance'));
}
function isCosmeticMismatch(specLine, codeName) {
    const specNorm = specLine.toLowerCase().replace(/[\s_-]/g, '');
    const codeNorm = codeName.toLowerCase().replace(/[\s_-]/g, '');
    return (specNorm !== codeNorm &&
        specNorm.length > 3 &&
        codeNorm.length > 3 &&
        (specNorm.includes(codeNorm) || codeNorm.includes(specNorm)));
}
function isImplementationDetail(conflict) {
    const implKeywords = ['implementation', 'refactor', 'rename', 'move', 'extract'];
    const lower = conflict.description.toLowerCase();
    return implKeywords.some((kw) => lower.includes(kw));
}
//# sourceMappingURL=conflict-resolver.js.map