"use strict";
// v1.12.0 — StoryParser.
//
// Parses the bolded-field markdown format used in docs/stories/.
// Pure (no filesystem) — takes `(content, sourcePath)` and returns
// `{ value, errors }`. Tolerant: unknown fields are kept in
// `fields`, unknown sections in `sections`.
Object.defineProperty(exports, "__esModule", { value: true });
exports._internals = void 0;
exports.parseStory = parseStory;
exports.parseEpic = parseEpic;
const StorySchema_1 = require("./StorySchema");
const FIELD_LINE_RE = /^\*\*([A-Za-z][A-Za-z ]*?):\*\*\s*(.+?)\s*$/;
const HEADING_RE = /^##\s+(.+?)\s*$/;
const TITLE_RE = /^#\s+(.*)$/;
const AC_LINE_RE = /^- \[([ xX])\]\s+(.+?)\s*$/;
const CHANGELOG_LINE_RE = /^-\s+([0-9TZ:.\- ]+?)\s+[—-]\s+(.+?)\s+[—-]\s+(.+?)\s*$/;
const STORY_ID_RE = /\bStory\s+(\d+\.\d+(?:\.\d+)?)\b/g;
const FILENAME_STORY_ID_RE = /^(\d+\.\d+(?:\.\d+)?)\.story\.md$/i;
function normalizeKey(field) {
    // "Depends on" → "dependsOn"; "StoryId" stays; "Discovered in phase" → "discoveredInPhase"
    const parts = field.trim().split(/\s+/);
    return parts
        .map((p, i) => (i === 0 ? p.charAt(0).toLowerCase() + p.slice(1) : p.charAt(0).toUpperCase() + p.slice(1)))
        .join('');
}
function stripBackticks(v) {
    return v.replace(/^`(.*)`$/, '$1').trim();
}
function splitCommaList(v) {
    return v
        .split(/,/)
        .map(s => stripBackticks(s.trim()))
        .filter(s => s.length > 0);
}
function parseRaw(content) {
    const fields = {};
    const sections = {};
    let title = '';
    const lines = content.split(/\r?\n/);
    let currentSection = null;
    const sectionBuf = [];
    const flushSection = () => {
        if (currentSection !== null) {
            sections[currentSection] = sectionBuf.join('\n').replace(/\s+$/g, '');
        }
    };
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        if (i === 0) {
            const m = TITLE_RE.exec(line);
            if (m) {
                title = m[1].trim();
                continue;
            }
        }
        const headingMatch = HEADING_RE.exec(line);
        if (headingMatch) {
            flushSection();
            currentSection = headingMatch[1].trim().toLowerCase();
            sectionBuf.length = 0;
            continue;
        }
        if (currentSection === null) {
            const fieldMatch = FIELD_LINE_RE.exec(line);
            if (fieldMatch) {
                const key = normalizeKey(fieldMatch[1]);
                fields[key] = stripBackticks(fieldMatch[2]);
                continue;
            }
        }
        else {
            sectionBuf.push(line);
        }
    }
    flushSection();
    return { fields, sections, title };
}
function parseAcceptanceCriteria(sectionText) {
    if (!sectionText)
        return [];
    const out = [];
    for (const line of sectionText.split(/\r?\n/)) {
        const m = AC_LINE_RE.exec(line);
        if (m)
            out.push({ text: m[2].trim(), checked: m[1].toLowerCase() === 'x' });
    }
    return out;
}
function parseChangeLog(sectionText) {
    if (!sectionText)
        return [];
    const out = [];
    for (const line of sectionText.split(/\r?\n/)) {
        if (!line.trim().startsWith('-'))
            continue;
        const m = CHANGELOG_LINE_RE.exec(line);
        if (m) {
            out.push({ date: m[1].trim(), author: m[2].trim(), note: m[3].trim(), raw: line });
        }
        else {
            // Keep unparseable bullet so callers can still see it as raw.
            out.push({ date: '', author: '', note: line.replace(/^-\s+/, '').trim(), raw: line });
        }
    }
    return out;
}
function extractStoryIds(sectionText) {
    if (!sectionText)
        return [];
    const ids = new Set();
    const re = new RegExp(STORY_ID_RE.source, 'g');
    let m;
    while ((m = re.exec(sectionText)) !== null)
        ids.add(m[1]);
    // Also pick up table rows / hyperlinks like "[3.3](3.3.story.md)".
    const linkRe = /\[(\d+\.\d+(?:\.\d+)?)\]\(/g;
    while ((m = linkRe.exec(sectionText)) !== null)
        ids.add(m[1]);
    return Array.from(ids).sort();
}
function deriveEpicIdFromPath(sourcePath) {
    const norm = sourcePath.replace(/\\/g, '/');
    const parts = norm.split('/');
    // .../docs/stories/<epic-id>/<file>.md
    if (parts.length >= 2)
        return parts[parts.length - 2];
    return '';
}
function deriveStoryIdFromPath(sourcePath) {
    const norm = sourcePath.replace(/\\/g, '/');
    const base = norm.substring(norm.lastIndexOf('/') + 1);
    const m = FILENAME_STORY_ID_RE.exec(base);
    return m ? m[1] : null;
}
function parseStory(content, sourcePath) {
    const errors = [];
    const { fields, sections, title } = parseRaw(content);
    let id = fields.storyId || '';
    if (!id) {
        const fromPath = deriveStoryIdFromPath(sourcePath);
        if (fromPath)
            id = fromPath;
    }
    if (!id) {
        errors.push({ sourcePath, message: 'missing StoryId field and filename does not match N.M.story.md' });
    }
    const epicIdField = fields.epic || '';
    let epicId = epicIdField.split(/\s+/)[0] || '';
    if (!epicId) {
        epicId = deriveEpicIdFromPath(sourcePath);
    }
    if (!epicId) {
        errors.push({ sourcePath, message: 'missing Epic field and parent directory does not look like an epic' });
    }
    const statusRaw = fields.status || '';
    if (!statusRaw)
        errors.push({ sourcePath, message: 'missing Status field' });
    let status = 'Draft';
    if (statusRaw && (0, StorySchema_1.isStoryStatus)(statusRaw))
        status = statusRaw;
    else if (statusRaw)
        errors.push({ sourcePath, message: `unknown Status value: "${statusRaw}"` });
    const dependsOn = fields.dependsOn ? splitCommaList(fields.dependsOn).flatMap(s => {
        // "Story 3.1" → "3.1"; "3.1 (host enum)" → "3.1"; plain "3.1" stays.
        const m = /\b(\d+\.\d+(?:\.\d+)?)\b/.exec(s);
        return m ? [m[1]] : [];
    }) : [];
    const story = {
        id,
        epicId,
        title: title.replace(/^Story\s+\d+(\.\d+){1,2}\s*[—-]\s*/, ''),
        status,
        severity: fields.severity || undefined,
        cluster: fields.cluster || undefined,
        dependsOn,
        fields,
        sections,
        acceptanceCriteria: parseAcceptanceCriteria(sections['acceptance criteria']),
        changeLog: parseChangeLog(sections['change log']),
        description: sections['description'],
        devNotes: sections['dev notes'],
        fileList: sections['file list'],
        sourcePath,
        raw: content,
    };
    return errors.length > 0 ? { value: story, errors } : { value: story, errors: [] };
}
function parseEpic(content, sourcePath) {
    const errors = [];
    const { fields, sections, title } = parseRaw(content);
    let id = fields.epicId || '';
    if (!id)
        id = deriveEpicIdFromPath(sourcePath);
    if (!id) {
        errors.push({ sourcePath, message: 'missing EpicId field and parent directory not identifiable' });
    }
    const statusRaw = fields.status || '';
    const status = statusRaw && (0, StorySchema_1.isStoryStatus)(statusRaw) ? statusRaw : undefined;
    if (statusRaw && !status) {
        errors.push({ sourcePath, message: `unknown epic Status value: "${statusRaw}"` });
    }
    const storyIds = extractStoryIds(sections['stories'] || sections['recommended execution order']);
    const epic = {
        id,
        title: title.replace(/^Epic:\s*/, ''),
        status,
        goal: sections['goal'],
        scope: sections['scope'],
        fields,
        sections,
        storyIds,
        acceptanceCriteria: parseAcceptanceCriteria(sections['acceptance criteria']),
        sourcePath,
        raw: content,
    };
    return { value: epic, errors };
}
// Exported for tests so we can introspect the raw extraction step.
exports._internals = {
    parseRaw,
    parseAcceptanceCriteria,
    parseChangeLog,
    extractStoryIds,
    deriveEpicIdFromPath,
    deriveStoryIdFromPath,
    normalizeKey,
};
