/**
 * Pure tree-builder: turn a flat array of sidebar-style page nodes (as produced
 * by `enumerateSpacePages`) into a nested tree.
 *
 * Input: a flat array of nodes. Each node is expected to carry at least
 *   { id, slugId, title, position, parentPageId } (extra fields are ignored).
 *
 * Output: an array of ROOT nodes, each shaped as
 *   { id, slugId, title, children? }
 * where `children` is the array of child nodes (same shape, recursively). The
 * `children` key is OMITTED entirely when a node has no children — consistent
 * with how `filterPage` omits an empty `subpages` array — to keep the payload
 * lean (nesting alone conveys the structure; parentPageId/position/hasChildren
 * are intentionally dropped from the output).
 *
 * Linking rule: a node is attached as a child of `parentPageId` only when that
 * parent id is actually present in the input. Otherwise — including a null /
 * undefined `parentPageId`, or a parent that was capped out of the bounded walk
 * — the node is promoted to a ROOT. So "orphan whose parent is missing" is the
 * defined behavior: it surfaces at the top level rather than disappearing.
 *
 * Ordering rule: the roots array and every `children` array are sorted ascending
 * by the node's `position` string. The comparator is a plain code-unit (byte)
 * comparison — NOT localeCompare — because the server orders sidebar pages by
 * `collate "C"` (byte order), which a raw `<`/`>` compare approximates for the
 * fractional-index ASCII keys (e.g. "a0", "a1"). Nodes with a missing/undefined
 * `position` sort last.
 *
 * Pure: no I/O, no network, deterministic.
 */
export function buildPageTree(nodes) {
    // Map id -> output node. Build the lean output shape up front.
    const byId = new Map();
    // Preserve the original position string for sorting (kept off the output).
    const positionById = new Map();
    for (const node of nodes) {
        if (!node || typeof node !== "object" || !node.id)
            continue;
        // Defensive against duplicate ids: last one wins (overwrites the earlier
        // entry). `enumerateSpacePages` already dedups, so this is belt-and-braces.
        byId.set(node.id, {
            id: node.id,
            slugId: node.slugId,
            title: node.title,
        });
        positionById.set(node.id, node.position);
    }
    // Stable comparator on the position string: code-unit order, missing last.
    const byPosition = (aId, bId) => {
        const a = positionById.get(aId);
        const b = positionById.get(bId);
        if (a === undefined || a === null)
            return b === undefined || b === null ? 0 : 1;
        if (b === undefined || b === null)
            return -1;
        if (a < b)
            return -1;
        if (a > b)
            return 1;
        return 0;
    };
    const roots = [];
    const childrenIdsByParent = new Map();
    for (const node of nodes) {
        if (!node || typeof node !== "object" || !node.id)
            continue;
        const parentId = node.parentPageId;
        // Child only when the parent is actually present in the input; otherwise
        // (null/undefined parent, or parent capped out of the walk) -> root.
        if (parentId && byId.has(parentId)) {
            const list = childrenIdsByParent.get(parentId) ?? [];
            list.push(node.id);
            childrenIdsByParent.set(parentId, list);
        }
        else {
            roots.push(node.id);
        }
    }
    // Attach sorted children arrays to each parent, omitting empty ones.
    for (const [parentId, childIds] of childrenIdsByParent) {
        const parent = byId.get(parentId);
        if (!parent)
            continue;
        childIds.sort(byPosition);
        parent.children = childIds.map((id) => byId.get(id));
    }
    roots.sort(byPosition);
    return roots.map((id) => byId.get(id));
}
