const TRAJECTORY_HEADING = /^##\s+Test Trajectory\b/;
const DEFERRED_HEADING = /^###\s+Deferred Verification\b/;
const NEXT_SECTION_HEADING = /^#{1,3}\s+/;
const PHASE_REFERENCE = /^\s*Phase\s+(\d+)\s*$/i;
const VALID_STATES = new Set([
    "planned",
    "writable",
    "written",
    "passing",
    "blocked",
    "skipped",
    "unknown",
]);
const VALID_KINDS = new Set([
    "example",
    "property",
    "contract",
    "approval",
    "formal",
]);
const VALID_SCOPES = new Set(["unit", "integration", "e2e"]);
/**
 * Parse a markdown table row into cells. Strips the leading/trailing pipe and
 * splits on unescaped pipes. Does NOT handle escaped pipes inside cell content
 * — the trajectory asserts column does not contain pipes in practice.
 */
function parseTableRow(line) {
    const trimmed = line.trim();
    if (!trimmed.startsWith("|") || !trimmed.endsWith("|"))
        return [];
    return trimmed
        .slice(1, -1)
        .split("|")
        .map((cell) => cell.trim());
}
/**
 * Normalize a header cell to a canonical key. Case-insensitive, trims
 * whitespace, maps variant spellings to canonical names.
 */
function normalizeHeader(header) {
    const normalized = header.toLowerCase().replace(/\s+/g, " ").trim();
    const aliases = {
        id: "id",
        asserts: "asserts",
        "writable at": "writableAt",
        "passes at": "passesAt",
        state: "state",
        kind: "kind",
        scope: "scope",
    };
    return aliases[normalized] ?? normalized;
}
/**
 * Parse a "Phase N" cell into a number. Returns NaN if the cell is not a
 * valid phase reference. The validator catches NaN and emits a specific error.
 */
function parsePhaseReference(cell) {
    const match = cell.match(PHASE_REFERENCE);
    if (!match)
        return Number.NaN;
    return Number.parseInt(match[1], 10);
}
function parseState(cell) {
    const normalized = cell.toLowerCase().trim();
    if (VALID_STATES.has(normalized)) {
        return normalized;
    }
    return "unknown";
}
function parseOptionalKind(cell) {
    const normalized = cell.toLowerCase().trim();
    if (!normalized)
        return undefined;
    if (VALID_KINDS.has(normalized)) {
        return normalized;
    }
    return undefined;
}
function parseOptionalScope(cell) {
    const normalized = cell.toLowerCase().trim();
    if (!normalized)
        return undefined;
    if (VALID_SCOPES.has(normalized)) {
        return normalized;
    }
    return undefined;
}
/**
 * Extract the block of lines under `## Test Trajectory` up to the next
 * heading of equal or lesser depth. The trajectory block includes the
 * main table; the `### Deferred Verification` subsection is extracted
 * separately.
 */
function extractTrajectoryBlock(lines) {
    let inTrajectory = false;
    let inDeferred = false;
    const tableLines = [];
    const deferredLines = [];
    for (const line of lines) {
        if (TRAJECTORY_HEADING.test(line)) {
            inTrajectory = true;
            inDeferred = false;
            continue;
        }
        if (!inTrajectory)
            continue;
        if (DEFERRED_HEADING.test(line)) {
            inDeferred = true;
            continue;
        }
        // A new top-level or second-level heading ends the trajectory block.
        // A third-level heading other than Deferred Verification also ends it.
        if (NEXT_SECTION_HEADING.test(line) && !DEFERRED_HEADING.test(line)) {
            const depth = line.match(/^(#{1,6})/)?.[1].length ?? 0;
            if (depth <= 2)
                break;
            // A ### heading that isn't Deferred Verification ends the trajectory
            if (depth === 3)
                break;
        }
        if (inDeferred) {
            deferredLines.push(line);
        }
        else {
            tableLines.push(line);
        }
    }
    return { tableLines, deferredLines };
}
/**
 * Parse a Deferred Verification block. Each deferred item is a top-level
 * bullet with three sub-bullets:
 *
 * - **{name}**
 *   - reason: {text}
 *   - would require: {text}
 *   - mitigation: {text}
 */
function parseDeferredBlock(lines) {
    const rows = [];
    let current = null;
    const flush = () => {
        if (current && current.name !== undefined) {
            rows.push({
                name: current.name,
                reason: current.reason ?? "",
                wouldRequire: current.wouldRequire ?? "",
                mitigation: current.mitigation ?? "",
            });
        }
        current = null;
    };
    for (const rawLine of lines) {
        const line = rawLine.replace(/\s+$/, "");
        // Top-level bullet with bolded name: - **{name}**
        const nameMatch = line.match(/^-\s+\*\*(.+?)\*\*\s*(?:—\s*(.*))?$/);
        if (nameMatch) {
            flush();
            current = { name: nameMatch[1].trim() };
            // Handle inline one-line form: - **Name** — reason: X — would require: Y — mitigation: Z
            const rest = nameMatch[2];
            if (rest) {
                const reasonMatch = rest.match(/reason:\s*([^—]+?)(?:\s*—|$)/i);
                const wrMatch = rest.match(/would require:\s*([^—]+?)(?:\s*—|$)/i);
                const mitMatch = rest.match(/mitigation:\s*(.+)$/i);
                if (reasonMatch)
                    current.reason = reasonMatch[1].trim();
                if (wrMatch)
                    current.wouldRequire = wrMatch[1].trim();
                if (mitMatch)
                    current.mitigation = mitMatch[1].trim();
            }
            continue;
        }
        if (!current)
            continue;
        // Sub-bullet: - reason: / - would require: / - mitigation:
        const subMatch = line.match(/^\s+-\s+(reason|would require|mitigation):\s*(.*)$/i);
        if (subMatch) {
            const key = subMatch[1].toLowerCase();
            const value = subMatch[2].trim();
            if (key === "reason")
                current.reason = value;
            else if (key === "would require")
                current.wouldRequire = value;
            else if (key === "mitigation")
                current.mitigation = value;
        }
    }
    flush();
    return rows;
}
/**
 * Parse the trajectory table. Returns rows parsed from the GFM table under
 * `## Test Trajectory`. Unknown column headers are ignored. Rows with
 * missing required columns are skipped — the validator catches structural
 * problems and surfaces them.
 */
function parseTrajectoryTable(lines) {
    const tableLines = lines.filter((line) => line.trim().startsWith("|"));
    if (tableLines.length < 2)
        return [];
    const headerCells = parseTableRow(tableLines[0]);
    const separator = parseTableRow(tableLines[1]);
    // Separator row looks like |----|----|... — every cell is dashes/colons
    const isSeparator = separator.every((cell) => /^:?-+:?$/.test(cell));
    if (!isSeparator)
        return [];
    const columnKeys = headerCells.map(normalizeHeader);
    const rows = [];
    for (let i = 2; i < tableLines.length; i++) {
        const cells = parseTableRow(tableLines[i]);
        if (cells.length !== columnKeys.length)
            continue;
        const record = {};
        for (let j = 0; j < columnKeys.length; j++) {
            record[columnKeys[j]] = cells[j];
        }
        const id = record.id?.trim();
        const asserts = record.asserts?.trim();
        if (!id || !asserts)
            continue;
        rows.push({
            id,
            asserts,
            writableAt: parsePhaseReference(record.writableAt ?? ""),
            passesAt: parsePhaseReference(record.passesAt ?? ""),
            state: parseState(record.state ?? ""),
            kind: parseOptionalKind(record.kind ?? ""),
            scope: parseOptionalScope(record.scope ?? ""),
        });
    }
    return rows;
}
/**
 * Parse a Test Trajectory from an impl.md body (the content after the
 * frontmatter). Returns an empty trajectory with `present: false` when the
 * `## Test Trajectory` section is absent. Never throws — errors are surfaced
 * by the validator, not the parser.
 */
export function parseTrajectory(body) {
    const lines = body.split("\n");
    const hasTrajectory = lines.some((line) => TRAJECTORY_HEADING.test(line));
    if (!hasTrajectory) {
        return { rows: [], deferred: [], present: false };
    }
    const { tableLines, deferredLines } = extractTrajectoryBlock(lines);
    const rows = parseTrajectoryTable(tableLines);
    const deferred = parseDeferredBlock(deferredLines);
    return { rows, deferred, present: true };
}
