// engine/provider-adapters/shared/spec-loader.ts — SPEC-670: Load + parse the unified spec.md.
//
// Per SPEC-630 the canonical spec is unified: a single `spec.md` file holds frontmatter
// + Problem + Acceptance Criteria + Technical sections. There is **no** separate
// technical.md — its content lives inside `## Technical` of `spec.md`.
//
// This loader reads `spec.md` once, splits it into the views every renderer needs
// (specBody, technicalBody, signatures, scenarios, file lists) and surfaces non-fatal
// parser issues in `warnings` instead of throwing.
import { readFile } from 'node:fs/promises';
import { specStore } from '../../../storage/index.js';
import { stripFrontmatter } from '../../frontmatter-parser.js';
import { extractSignatures } from './markdown-utils.js';
/**
 * Load + parse the unified `spec.md` for a given spec ID.
 * Never throws on parse issues — surfaces them via `LoadedSpec.warnings`.
 *
 * Throws only when the spec is not found in the store, or when `spec.specPath`
 * is missing on disk (genuine I/O failure).
 */
export async function loadSpecForRendering(projectId, projectPath, specId) {
    void projectPath;
    const spec = await specStore.getSpec(projectId, specId);
    if (!spec) {
        throw new Error(`Spec "${specId}" not found in project "${projectId}". Use list_specs to verify the ID.`);
    }
    const warnings = [];
    let raw;
    try {
        raw = await readFile(spec.specPath, 'utf-8');
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        throw new Error(`Cannot read canonical spec.md at "${spec.specPath}": ${msg}. ` +
            `Run heal_spec_docs to regenerate it.`, { cause: err });
    }
    const specBody = stripFrontmatter(raw);
    // Empty body → minimal stub (Scenario 12 handled here so every adapter can rely on
    // a well-shaped LoadedSpec without re-checking emptiness).
    if (specBody.trim().length === 0) {
        warnings.push('Spec body is empty — author has not yet filled in problem/criteria');
        return {
            spec,
            specBody: '',
            technicalBody: null,
            signatures: [],
            scenarios: [],
            filesToCreate: [],
            filesToModify: [],
            warnings,
        };
    }
    const technicalBody = extractSection(specBody, /^##\s+Technical\b/im);
    if (!technicalBody || technicalBody.trim().length === 0) {
        warnings.push('No technical section found');
    }
    const signatures = technicalBody ? extractSignatures(technicalBody) : [];
    const scenarios = parseScenarios(specBody);
    const filesToCreate = technicalBody
        ? parseFileList(technicalBody, /^###\s+Files to create\b/im)
        : [];
    const filesToModify = technicalBody
        ? parseFileList(technicalBody, /^###\s+Files to modify\b/im)
        : [];
    return {
        spec,
        specBody,
        technicalBody,
        signatures,
        scenarios,
        filesToCreate,
        filesToModify,
        warnings,
    };
}
/**
 * Extract the body of an H2/H3 section starting at a heading matching `headingRe`,
 * up to the next heading of equal-or-higher level. Returns null if not found.
 */
export function extractSection(body, headingRe) {
    const lines = body.split('\n');
    const startIdx = lines.findIndex((l) => headingRe.test(l));
    if (startIdx === -1) {
        return null;
    }
    const startMatch = /^(#{2,6})\s/.exec(lines[startIdx] ?? '');
    if (!startMatch?.[1]) {
        return null;
    }
    const startLevel = startMatch[1].length;
    let endIdx = lines.length;
    for (let i = startIdx + 1; i < lines.length; i += 1) {
        const m = /^(#{2,6})\s/.exec(lines[i] ?? '');
        if (m?.[1] && m[1].length <= startLevel) {
            endIdx = i;
            break;
        }
    }
    return lines.slice(startIdx + 1, endIdx).join('\n');
}
/**
 * Parse BDD scenarios from `## Acceptance Criteria` of the body.
 *
 * Recognized format (per SPEC-630):
 *   **Scenario N: title**
 *   - GIVEN ...
 *   - WHEN ...
 *   - THEN ...
 *
 * Only scenarios with all three of GIVEN/WHEN/THEN are returned (Scenario 11 graceful
 * degradation — malformed scenarios are dropped silently).
 */
function parseScenarios(body) {
    const acSection = extractSection(body, /^##\s+Acceptance Criteria\b/im);
    if (!acSection) {
        return [];
    }
    const out = [];
    // Split into scenario blocks by `**Scenario` markers.
    const blockRe = /\*\*Scenario[^*]*\*\*[\s\S]*?(?=\*\*Scenario|$)/g;
    const blocks = acSection.match(blockRe) ?? [];
    for (const block of blocks) {
        const titleMatch = /\*\*Scenario[^*:]*:?\s*([^*]+)\*\*/.exec(block);
        const title = titleMatch?.[1]?.trim() ?? 'Untitled scenario';
        const given = extractKeyword(block, 'GIVEN');
        const when = extractKeyword(block, 'WHEN');
        const then = extractKeyword(block, 'THEN');
        if (given && when && then) {
            out.push({ title, given, when, then });
        }
    }
    return out;
}
function extractKeyword(block, keyword) {
    const re = new RegExp(`-\\s*${keyword}\\s+([^\\n]+)`, 'i');
    const m = re.exec(block);
    return m?.[1]?.trim() ?? null;
}
/**
 * Parse a list of file paths from a section starting at `headingRe`. The section is
 * expected to be a markdown bullet list of `path` or `path — comment`. Backticks and
 * trailing prose after `—`/`-` are trimmed.
 */
function parseFileList(body, headingRe) {
    const section = extractSection(body, headingRe);
    if (!section) {
        return [];
    }
    const out = [];
    for (const line of section.split('\n')) {
        const trimmed = line.trim();
        if (!trimmed.startsWith('-')) {
            continue;
        }
        let item = trimmed.replace(/^-\s*/, '').trim();
        item = item.replace(/^`([^`]+)`.*$/, '$1');
        item = item.replace(/\s+[—–-]\s+.*$/, '').trim();
        if (item.length > 0) {
            out.push(item);
        }
    }
    return out;
}
//# sourceMappingURL=spec-loader.js.map