/**
 * TipTap node tree → walker-style block list.
 *
 * The matcher and fingerprint operate on a flat ordered block list (one entry
 * per block, pre-order traversal, with parentPosition pointing back). This
 * adapter converts the TipTap document tree into that shape so the matcher
 * operates on the SAME structural view as the editor — not on a separately
 * re-parsed markdown body.
 *
 * Avoiding a second parse is the architectural defense against walker-vs-
 * parser drift: one block tree feeds both TipTap rendering and node identity
 * tracking.
 *
 * adr: adr/node-identity-matcher.md
 */
/**
 * Flatten a TipTap document tree into a walker-style block list.
 *
 * Container blocks (lists, blockquotes, listItems) get an entry AND their
 * children get separate entries with parentPosition pointing back. Container
 * text is the first direct paragraph's content only (no recursive roll-up).
 */
export function tiptapToBlocks(doc) {
    const blocks = [];
    walkNodes(doc.content || [], blocks, null);
    return blocks;
}
const CONTAINER_TYPES = new Set([
    'bulletList',
    'orderedList',
    'taskList',
    'blockquote',
    'table',
    'listItem',
    'taskItem',
    'tableRow',
    'tableCell',
    'tableHeader',
    'footnoteSection',
    'footnoteDefinition',
]);
function walkNodes(nodes, blocks, parentPosition) {
    let ordinalInParent = 0;
    for (const node of nodes) {
        if (node.type === 'heading') {
            const level = node.attrs?.level || 1;
            const text = extractInlineText(node.content || []);
            blocks.push({
                position: blocks.length,
                type: 'heading',
                level,
                text,
                parentPosition,
                ordinalInParent: ordinalInParent++,
                inlineMarks: countInlineMarks(node.content || []),
                id: node.attrs?.id,
            });
        }
        else if (node.type === 'paragraph') {
            const text = extractInlineText(node.content || []);
            blocks.push({
                position: blocks.length,
                type: 'paragraph',
                text,
                parentPosition,
                ordinalInParent: ordinalInParent++,
                inlineMarks: countInlineMarks(node.content || []),
                id: node.attrs?.id,
            });
        }
        else if (node.type === 'bulletList' || node.type === 'orderedList' || node.type === 'taskList') {
            const containerPosition = blocks.length;
            blocks.push({
                position: containerPosition,
                type: node.type,
                text: '',
                parentPosition,
                ordinalInParent: ordinalInParent++,
                id: node.attrs?.id,
            });
            walkNodes(node.content || [], blocks, containerPosition);
        }
        else if (node.type === 'listItem' || node.type === 'taskItem') {
            const itemPosition = blocks.length;
            // listItem's identity text = first direct paragraph child only.
            // Avoids cascading fingerprint changes when nested children are edited.
            const firstParaText = firstParagraphText(node.content || []);
            blocks.push({
                position: itemPosition,
                type: node.type,
                text: firstParaText,
                parentPosition,
                ordinalInParent: ordinalInParent++,
                id: node.attrs?.id,
            });
            walkNodes(node.content || [], blocks, itemPosition);
        }
        else if (node.type === 'blockquote') {
            const bqPosition = blocks.length;
            blocks.push({
                position: bqPosition,
                type: 'blockquote',
                text: '',
                parentPosition,
                ordinalInParent: ordinalInParent++,
                id: node.attrs?.id,
            });
            walkNodes(node.content || [], blocks, bqPosition);
        }
        else if (node.type === 'footnoteSection') {
            // Container holding all `footnoteDefinition`s at end-of-doc. Treated
            // like a blockquote: container fingerprint is content-empty, identity
            // travels through the matcher's structural rules. The serializer
            // enforces end-of-doc position regardless of where it appears in the
            // tree, so position-stability is guaranteed at the boundary.
            // adr: adr/footnote-system.md
            const sectionPosition = blocks.length;
            blocks.push({
                position: sectionPosition,
                type: 'footnoteSection',
                text: '',
                parentPosition,
                ordinalInParent: ordinalInParent++,
                id: node.attrs?.id,
            });
            walkNodes(node.content || [], blocks, sectionPosition);
        }
        else if (node.type === 'footnoteDefinition') {
            // Container for one footnote's content (typically a single paragraph,
            // occasionally multiple). The label attr round-trips with the node;
            // the matcher fingerprints by content + slot like any container.
            // adr: adr/footnote-system.md
            const defPosition = blocks.length;
            blocks.push({
                position: defPosition,
                type: 'footnoteDefinition',
                text: firstParagraphText(node.content || []),
                parentPosition,
                ordinalInParent: ordinalInParent++,
                id: node.attrs?.id,
            });
            walkNodes(node.content || [], blocks, defPosition);
        }
        else if (node.type === 'codeBlock') {
            const text = extractInlineText(node.content || []);
            blocks.push({
                position: blocks.length,
                type: 'codeBlock',
                language: node.attrs?.language || '',
                text,
                parentPosition,
                ordinalInParent: ordinalInParent++,
                id: node.attrs?.id,
            });
        }
        else if (node.type === 'horizontalRule') {
            blocks.push({
                position: blocks.length,
                type: 'horizontalRule',
                text: '',
                parentPosition,
                ordinalInParent: ordinalInParent++,
                id: node.attrs?.id,
            });
        }
        else if (node.type === 'table') {
            const tablePosition = blocks.length;
            blocks.push({
                position: tablePosition,
                type: 'table',
                text: extractTableText(node),
                parentPosition,
                ordinalInParent: ordinalInParent++,
                id: node.attrs?.id,
            });
            // Don't descend into table internals — the walker treats tables as opaque.
        }
        else if (node.type === 'image') {
            blocks.push({
                position: blocks.length,
                type: 'image',
                text: node.attrs?.alt || '',
                parentPosition,
                ordinalInParent: ordinalInParent++,
                id: node.attrs?.id,
            });
        }
        else if (CONTAINER_TYPES.has(node.type)) {
            // Generic container — emit entry, descend
            const containerPosition = blocks.length;
            blocks.push({
                position: containerPosition,
                type: node.type,
                text: '',
                parentPosition,
                ordinalInParent: ordinalInParent++,
                id: node.attrs?.id,
            });
            walkNodes(node.content || [], blocks, containerPosition);
        }
        else if (node.content) {
            // Unknown wrapper — descend without emitting
            walkNodes(node.content, blocks, parentPosition);
        }
    }
}
/** Get plain text from a node's inline children. */
function extractInlineText(nodes) {
    return nodes.map((n) => n.text || '').join('');
}
/** Find the first paragraph child (direct or via nested ordering) and return its text. */
function firstParagraphText(nodes) {
    for (const n of nodes) {
        if (n.type === 'paragraph')
            return extractInlineText(n.content || []);
    }
    return '';
}
/** Sum inline marks across all inline children. */
function countInlineMarks(nodes) {
    let bold = 0, italic = 0, links = 0, code = 0;
    for (const n of nodes) {
        if (n.type !== 'text')
            continue;
        for (const mark of n.marks || []) {
            if (mark.type === 'bold')
                bold++;
            else if (mark.type === 'italic')
                italic++;
            else if (mark.type === 'link')
                links++;
            else if (mark.type === 'code')
                code++;
        }
    }
    return { bold, italic, links, code };
}
/** Flatten table cell text — table is treated as an opaque block for fingerprinting. */
function extractTableText(table) {
    const parts = [];
    function walk(nodes) {
        if (!nodes)
            return;
        for (const n of nodes) {
            if (n.type === 'text')
                parts.push(n.text || '');
            else if (n.content)
                walk(n.content);
        }
    }
    walk(table.content);
    return parts.join(' ');
}
/**
 * Apply pinned IDs from a matcher result back onto the TipTap node tree.
 * Walks the tree in the same pre-order the adapter used and assigns
 * attrs.id from the corresponding pinned entry.
 */
export function applyIdsToTiptap(doc, pinnedByPosition) {
    let position = 0;
    function walk(nodes) {
        for (const node of nodes) {
            const isBlock = node.type === 'heading' ||
                node.type === 'paragraph' ||
                node.type === 'bulletList' ||
                node.type === 'orderedList' ||
                node.type === 'taskList' ||
                node.type === 'listItem' ||
                node.type === 'taskItem' ||
                node.type === 'blockquote' ||
                node.type === 'codeBlock' ||
                node.type === 'horizontalRule' ||
                node.type === 'table' ||
                node.type === 'image' ||
                node.type === 'footnoteSection' ||
                node.type === 'footnoteDefinition' ||
                CONTAINER_TYPES.has(node.type);
            if (isBlock) {
                const id = pinnedByPosition.get(position);
                if (id) {
                    if (!node.attrs)
                        node.attrs = {};
                    node.attrs.id = id;
                }
                position++;
                // Descend into container children, MIRRORING `walkNodes`/`tiptapToBlocks`.
                // CRITICAL: tables are opaque to the walker (see `tiptapToBlocks` —
                // emits one Block per table, doesn't descend). `applyIdsToTiptap` MUST
                // use the same descent rule, otherwise position counters diverge and a
                // matcher pin meant for a top-level node gets applied to a node inside
                // a table. That's exactly the table-row-stole-the-paragraph-id
                // corruption pattern (e.g. `tr:3141ee2a` where 3141ee2a was the
                // target-total paragraph's ID).
                //
                // Descend ONLY into types `walkNodes` recursively descends into.
                // adr: adr/node-identity-matcher.md
                const isContainer = node.type === 'bulletList' ||
                    node.type === 'orderedList' ||
                    node.type === 'taskList' ||
                    node.type === 'listItem' ||
                    node.type === 'taskItem' ||
                    node.type === 'blockquote' ||
                    node.type === 'footnoteSection' ||
                    node.type === 'footnoteDefinition';
                if (isContainer && node.content)
                    walk(node.content);
            }
            else if (node.content) {
                walk(node.content);
            }
        }
    }
    walk(doc.content || []);
}
