import { buildCodeFenceMask, SCENARIO_HEADER } from './requirement-text.js';
export function normalizeRequirementName(name) {
    return name.trim();
}
/**
 * Case- and whitespace-insensitive fold of a requirement name. Requirement
 * matching itself is case-sensitive (normalizeRequirementName); this fold
 * exists only for typo detection - near-miss REMOVED headers and the
 * RENAMED+REMOVED cross-section conflict - where two spellings that differ
 * only in case or interior whitespace mean a mistake, never two requirements.
 */
export function foldRequirementName(name) {
    return normalizeRequirementName(name).toLowerCase().replace(/\s+/g, ' ');
}
/** The canonical requirement header the delta reader recognizes. */
const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
/**
 * Extracts the Requirements section from a spec file and parses requirement blocks.
 */
export function extractRequirementsSection(content) {
    const normalized = normalizeLineEndings(content);
    const lines = normalized.split('\n');
    const fenceMask = buildCodeFenceMask(lines);
    const reqHeaderIndex = lines.findIndex((l, i) => !fenceMask[i] && /^##\s+Requirements\s*$/i.test(l));
    if (reqHeaderIndex === -1) {
        // No requirements section; create an empty one at the end
        const before = content.trimEnd();
        const headerLine = '## Requirements';
        return {
            before: before ? before + '\n\n' : '',
            headerLine,
            preamble: '',
            bodyBlocks: [],
            after: '\n',
        };
    }
    // Find end of this section: next line that starts with '## ' at same or higher level
    let endIndex = lines.length;
    for (let i = reqHeaderIndex + 1; i < lines.length; i++) {
        if (!fenceMask[i] && /^##\s+/.test(lines[i])) {
            endIndex = i;
            break;
        }
    }
    const before = lines.slice(0, reqHeaderIndex).join('\n');
    const headerLine = lines[reqHeaderIndex];
    const sectionBodyLines = lines.slice(reqHeaderIndex + 1, endIndex);
    const sectionBodyMask = fenceMask.slice(reqHeaderIndex + 1, endIndex);
    const isRequirementHeader = (cursor) => !sectionBodyMask[cursor] && REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor]);
    const isTopLevelHeader = (cursor) => !sectionBodyMask[cursor] && /^##\s+/.test(sectionBodyLines[cursor]);
    // Parse requirement blocks within section body
    const blocks = [];
    let cursor = 0;
    let preambleLines = [];
    // Collect preamble lines until first requirement header
    while (cursor < sectionBodyLines.length && !isRequirementHeader(cursor)) {
        preambleLines.push(sectionBodyLines[cursor]);
        cursor++;
    }
    while (cursor < sectionBodyLines.length) {
        const headerLineCandidate = sectionBodyLines[cursor];
        if (!isRequirementHeader(cursor)) {
            // Not a requirement header; skip line defensively
            cursor++;
            continue;
        }
        const headerMatch = headerLineCandidate.match(REQUIREMENT_HEADER_REGEX);
        const name = normalizeRequirementName(headerMatch[1]);
        cursor++;
        // Gather lines until next requirement header or end of section
        const bodyLines = [headerLineCandidate];
        while (cursor < sectionBodyLines.length && !isRequirementHeader(cursor) && !isTopLevelHeader(cursor)) {
            bodyLines.push(sectionBodyLines[cursor]);
            cursor++;
        }
        const raw = bodyLines.join('\n').trimEnd();
        blocks.push({ headerLine: headerLineCandidate, name, raw });
    }
    const after = lines.slice(endIndex).join('\n');
    const preamble = preambleLines.join('\n').trimEnd();
    return {
        before: before.trimEnd() ? before + '\n' : before,
        headerLine,
        preamble,
        bodyBlocks: blocks,
        after: after.startsWith('\n') ? after : '\n' + after,
    };
}
function normalizeLineEndings(content) {
    // Strip a UTF-8 BOM: Windows editors and PowerShell redirects prepend one,
    // and it would keep the first line's `## ADDED Requirements` from matching.
    return content.replace(/^﻿/, '').replace(/\r\n?/g, '\n');
}
/**
 * Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
 */
export function parseDeltaSpec(content) {
    const normalized = normalizeLineEndings(content);
    const lines = normalized.split('\n');
    const fenceMask = buildCodeFenceMask(lines);
    const sections = splitTopLevelSections(lines, fenceMask);
    const addedLookup = getSectionCaseInsensitive(sections, 'ADDED Requirements');
    const modifiedLookup = getSectionCaseInsensitive(sections, 'MODIFIED Requirements');
    const removedLookup = getSectionCaseInsensitive(sections, 'REMOVED Requirements');
    const renamedLookup = getSectionCaseInsensitive(sections, 'RENAMED Requirements');
    const skippedHeaders = [];
    const added = parseRequirementBlocksFromSection(addedLookup.body, {
        section: addedLookup.title,
        bodyStartLine: addedLookup.bodyStartLine,
        sink: skippedHeaders,
    });
    const modified = parseRequirementBlocksFromSection(modifiedLookup.body, {
        section: modifiedLookup.title,
        bodyStartLine: modifiedLookup.bodyStartLine,
        sink: skippedHeaders,
    });
    const removedNames = parseRemovedNames(removedLookup.body);
    const renamedPairs = parseRenamedPairs(renamedLookup.body);
    skippedHeaders.sort((a, b) => a.line - b.line);
    return {
        added,
        modified,
        removed: removedNames,
        renamed: renamedPairs,
        skippedHeaders,
        sectionPresence: {
            added: addedLookup.found,
            modified: modifiedLookup.found,
            removed: removedLookup.found,
            renamed: renamedLookup.found,
        },
    };
}
function splitTopLevelSections(lines, fenceMask) {
    const result = {};
    const indices = [];
    for (let i = 0; i < lines.length; i++) {
        if (fenceMask[i])
            continue;
        const m = lines[i].match(/^(##)\s+(.+)$/);
        if (m) {
            indices.push({ title: m[2].trim(), index: i });
        }
    }
    for (let i = 0; i < indices.length; i++) {
        const current = indices[i];
        const next = indices[i + 1];
        const end = next ? next.index : lines.length;
        result[current.title] = {
            lines: lines.slice(current.index + 1, end),
            fenceMask: fenceMask.slice(current.index + 1, end),
            bodyStartLine: current.index + 2,
        };
    }
    return result;
}
const EMPTY_SECTION_BODY = { lines: [], fenceMask: [], bodyStartLine: 0 };
function getSectionCaseInsensitive(sections, desired) {
    const target = desired.toLowerCase();
    for (const [title, body] of Object.entries(sections)) {
        if (title.toLowerCase() === target) {
            return { title, body, bodyStartLine: body.bodyStartLine, found: true };
        }
    }
    return { title: desired, body: EMPTY_SECTION_BODY, bodyStartLine: 0, found: false };
}
function parseRequirementBlocksFromSection(sectionBody, skipped) {
    const { lines, fenceMask } = sectionBody;
    if (lines.length === 0)
        return [];
    const isRequirementHeader = (i) => !fenceMask[i] && REQUIREMENT_HEADER_REGEX.test(lines[i]);
    const isTopLevelHeader = (i) => !fenceMask[i] && /^##\s+/.test(lines[i]);
    const recordIfSkippedHeader = (index) => {
        if (!skipped || fenceMask[index])
            return;
        const h3 = lines[index].match(/^###\s+(.+?)\s*$/);
        if (h3 && !REQUIREMENT_HEADER_REGEX.test(lines[index])) {
            skipped.sink.push({
                header: h3[1].trim(),
                section: skipped.section,
                line: skipped.bodyStartLine + index,
            });
        }
    };
    const blocks = [];
    let i = 0;
    while (i < lines.length) {
        // Seek next requirement header
        while (i < lines.length && !isRequirementHeader(i)) {
            recordIfSkippedHeader(i);
            i++;
        }
        if (i >= lines.length)
            break;
        const headerLine = lines[i];
        const m = headerLine.match(REQUIREMENT_HEADER_REGEX);
        if (!m) {
            i++;
            continue;
        }
        const name = normalizeRequirementName(m[1]);
        const buf = [headerLine];
        i++;
        while (i < lines.length && !isRequirementHeader(i) && !isTopLevelHeader(i)) {
            recordIfSkippedHeader(i);
            buf.push(lines[i]);
            i++;
        }
        blocks.push({ headerLine, name, raw: buf.join('\n').trimEnd() });
    }
    return blocks;
}
function parseRemovedNames(sectionBody) {
    const { lines, fenceMask } = sectionBody;
    if (lines.length === 0)
        return [];
    const names = [];
    for (let i = 0; i < lines.length; i++) {
        if (fenceMask[i])
            continue;
        const line = lines[i];
        const m = line.match(REQUIREMENT_HEADER_REGEX);
        if (m) {
            names.push(normalizeRequirementName(m[1]));
            continue;
        }
        // Also support bullet list of headers
        const bullet = line.match(/^\s*-\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
        if (bullet) {
            names.push(normalizeRequirementName(bullet[1]));
        }
    }
    return names;
}
function parseRenamedPairs(sectionBody) {
    const { lines, fenceMask } = sectionBody;
    if (lines.length === 0)
        return [];
    const pairs = [];
    let current = {};
    for (let i = 0; i < lines.length; i++) {
        if (fenceMask[i])
            continue;
        const line = lines[i];
        const fromMatch = line.match(/^\s*-?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
        const toMatch = line.match(/^\s*-?\s*TO:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
        if (fromMatch) {
            current.from = normalizeRequirementName(fromMatch[1]);
        }
        else if (toMatch) {
            current.to = normalizeRequirementName(toMatch[1]);
            if (current.from && current.to) {
                pairs.push({ from: current.from, to: current.to });
                current = {};
            }
        }
    }
    return pairs;
}
/**
 * Scenario names the current requirement block has and the incoming
 * (MODIFIED) block does not. A MODIFIED requirement replaces the whole block,
 * so every name reported here would be dropped from the main spec.
 *
 * Shared by archive (which refuses to apply the block) and validate (which
 * reports the same loss at authoring time, #1477), so the two cannot disagree
 * about what counts as a dropped scenario.
 */
export function findMissingCurrentScenarios(current, incoming) {
    // Multiplicity-aware: a name present N times in current and M times in
    // incoming means max(0, N - M) instances are missing. Set membership would
    // treat N>M as fully covered and let archive silently drop duplicates
    // (residual #1246 / duplicate-scenario-name blind spot).
    const remainingIncoming = new Map();
    for (const scenario of parseScenarioBlocks(incoming.raw)) {
        const name = scenario.name;
        remainingIncoming.set(name, (remainingIncoming.get(name) ?? 0) + 1);
    }
    const missing = [];
    for (const scenario of parseScenarioBlocks(current.raw)) {
        const name = scenario.name;
        const remaining = remainingIncoming.get(name) ?? 0;
        if (remaining > 0) {
            remainingIncoming.set(name, remaining - 1);
        }
        else {
            missing.push(name);
        }
    }
    return missing;
}
/**
 * Any non-fenced level-4 header on the given (masked) line. Reuses the spec
 * path's SCENARIO_HEADER so the two counters cannot drift apart.
 */
function scenarioHeaderAt(lines, mask, index) {
    return !mask[index] && SCENARIO_HEADER.test(lines[index]);
}
/**
 * The scenario name for a `#### ` header, matching the label the author reads:
 * the header text with the leading `####`, an optional CommonMark closing `#`
 * run (`#### Foo ####` renders as `Foo`), and an optional `Scenario:` prefix
 * stripped. Both the current and incoming blocks run through here, so the
 * comparison in findMissingCurrentScenarios stays internally consistent
 * regardless of label — and two headers that render to the same title (one
 * ATX-closed, one not) are not mistaken for a dropped scenario.
 */
function scenarioNameAt(line) {
    return line
        .replace(SCENARIO_HEADER, '')
        // Optional ATX closing sequence. CommonMark only treats a trailing `#` run
        // as a close when it is preceded by a space or tab — not any Unicode space —
        // so this uses `[ \t]`, not `\s`. A looser `\s` could strip a `#` run after
        // an exotic space (e.g. NBSP) that CommonMark keeps, folding two distinct
        // scenario names into one and masking a real loss. `[ \t]` keeps the fold
        // faithful to how the header actually renders.
        .replace(/[ \t]+#+[ \t]*$/, '')
        .replace(/^Scenario:\s*/i, '')
        .trim();
}
function parseScenarioBlocks(requirementRaw) {
    const lines = requirementRaw.replace(/\r\n?/g, '\n').split('\n');
    // A scenario is ANY non-fenced `#### ` header, matching the spec path's
    // SCENARIO_HEADER / countScenarios (requirement-text.ts) exactly — not only
    // `#### Scenario:`. The two MUST agree: a level-4 child whose header is not
    // literally `Scenario:` (e.g. `#### Edge case`) is still a scenario the spec
    // path counts, so a MODIFIED block that drops it would otherwise slip past
    // this loss check and be deleted by archive with no error (the parity the
    // SCENARIO_HEADER comment warns not to break). A `####` inside a fenced
    // example is masked out, matching countScenarios.
    const mask = buildCodeFenceMask(lines);
    const scenarios = [];
    let index = 0;
    while (index < lines.length) {
        if (!scenarioHeaderAt(lines, mask, index)) {
            index++;
            continue;
        }
        const start = index;
        const name = scenarioNameAt(lines[index]);
        index++;
        while (index < lines.length && !scenarioHeaderAt(lines, mask, index)) {
            index++;
        }
        scenarios.push({
            name,
            raw: lines.slice(start, index).join('\n').trimEnd(),
        });
    }
    return scenarios;
}
//# sourceMappingURL=requirement-blocks.js.map