// engine/reconcile/apply-changes.ts — SPEC-1011 Bug D: Apply approved text changes to spec.md
// Replacement is section-anchored to prevent silent cross-section mutations:
// the originalValue is searched only inside the named section's body (between its
// heading and the next heading at the same or shallower level), and the change is
// applied only when the match is unique within those bounds.
import { readFile } from 'node:fs/promises';
import { atomicWriteFile } from '../safety/atomic-write-file.js';
import { RECONCILE_METADATA_SECTIONS, } from '../../types/index.js';
function escapeRegex(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
 * Find the body bounds of a markdown section by heading name.
 * Returns the [start, end) offsets of the section body (after the heading line,
 * up to the next heading at the same or shallower level — or EOF).
 */
function findSectionBounds(content, section) {
    const headingRe = new RegExp(`^(#{1,6})\\s+${escapeRegex(section)}\\s*$`, 'mi');
    const match = headingRe.exec(content);
    if (!match) {
        return null;
    }
    const headingLevel = match[1]?.length ?? 0;
    const start = match.index + match[0].length;
    // Find next heading at same-or-shallower level
    const nextRe = new RegExp(`^#{1,${String(headingLevel)}}\\s+\\S`, 'gm');
    nextRe.lastIndex = start;
    const next = nextRe.exec(content);
    return { start, end: next ? next.index : content.length };
}
/**
 * Count non-overlapping occurrences of `needle` in `haystack`.
 */
function countOccurrences(haystack, needle) {
    if (needle.length === 0) {
        return 0;
    }
    let count = 0;
    let from = 0;
    let idx = haystack.indexOf(needle, from);
    while (idx !== -1) {
        count++;
        from = idx + needle.length;
        idx = haystack.indexOf(needle, from);
    }
    return count;
}
/**
 * Apply approved text changes to the spec.md body.
 *
 * For each non-metadata approved change:
 * 1. Locate the named section's body via heading anchor.
 * 2. Require exactly one occurrence of `originalValue` inside that section.
 * 3. Replace it in-place at the resolved absolute offset.
 *
 * Skips (with reason) when the section heading is absent, the value is missing
 * inside the section, or the value appears multiple times (ambiguous match).
 * Writes only when at least one substitution succeeded.
 */
export async function applyChangesToSpec(specPath, changes) {
    const approvedTextChanges = changes.filter((c) => c.approved && !RECONCILE_METADATA_SECTIONS.has(c.section));
    if (approvedTextChanges.length === 0) {
        return { applied: 0, skipped: [], fileWritten: false };
    }
    let content;
    try {
        content = await readFile(specPath, 'utf-8');
    }
    catch (err) {
        const reason = err instanceof Error ? err.message : String(err);
        return {
            applied: 0,
            skipped: approvedTextChanges.map((c) => ({
                section: c.section,
                originalValue: c.originalValue,
                reason: `Cannot read spec.md: ${reason}`,
            })),
            fileWritten: false,
        };
    }
    let modified = content;
    let applied = 0;
    const skipped = [];
    for (const change of approvedTextChanges) {
        const bounds = findSectionBounds(modified, change.section);
        if (bounds === null) {
            skipped.push({
                section: change.section,
                originalValue: change.originalValue,
                reason: `Section heading "${change.section}" not found in spec.md`,
            });
            continue;
        }
        const sectionBody = modified.slice(bounds.start, bounds.end);
        const occurrences = countOccurrences(sectionBody, change.originalValue);
        if (occurrences === 0) {
            skipped.push({
                section: change.section,
                originalValue: change.originalValue,
                reason: `originalValue not found in section "${change.section}"`,
            });
            continue;
        }
        if (occurrences > 1) {
            skipped.push({
                section: change.section,
                originalValue: change.originalValue,
                reason: `originalValue is ambiguous (${String(occurrences)} matches in section "${change.section}")`,
            });
            continue;
        }
        const relativeIdx = sectionBody.indexOf(change.originalValue);
        const absoluteIdx = bounds.start + relativeIdx;
        modified =
            modified.slice(0, absoluteIdx) +
                change.newValue +
                modified.slice(absoluteIdx + change.originalValue.length);
        applied++;
    }
    if (applied === 0) {
        return { applied: 0, skipped, fileWritten: false };
    }
    await atomicWriteFile(specPath, modified);
    return { applied, skipped, fileWritten: true };
}
//# sourceMappingURL=apply-changes.js.map