/**
 * Build the deterministic category tree consumed by `/docs`.
 *
 * Walks `docs/**` (excluding `docs/private/**` and `**\/archived/**`), resolves
 * frontmatter for each file, applies visibility filtering, and groups by
 * (category, subcategory). Sort order:
 *   category.order → subcategory.order → frontmatter.order → title.
 *
 * The category/subcategory display tree lives in
 * `docs/site/site_doc_manifest.yaml` under the top-level `categories:` key.
 * Per-doc `category` slugs reference that tree.
 */
import fs from "node:fs";
import path from "node:path";
import { resolveFrontmatter } from "./doc_frontmatter.js";
import { isVisible } from "./visibility.js";
const SKIP_DIR_NAMES = new Set(["private", "node_modules", ".git", "archived"]);
function compareStable(a, b) {
    return a < b ? -1 : a > b ? 1 : 0;
}
/** Recursive walker for markdown files under `docsRoot`. */
function collectMarkdownFiles(docsRoot) {
    const out = [];
    const walk = (absDir, rel) => {
        if (!fs.existsSync(absDir))
            return;
        for (const ent of fs.readdirSync(absDir, { withFileTypes: true })) {
            if (ent.isDirectory()) {
                if (SKIP_DIR_NAMES.has(ent.name))
                    continue;
                walk(path.join(absDir, ent.name), rel ? `${rel}/${ent.name}` : ent.name);
            }
            else if (ent.isFile() && ent.name.endsWith(".md")) {
                out.push(rel ? `${rel}/${ent.name}` : ent.name);
            }
        }
    };
    walk(docsRoot, "");
    out.sort(compareStable);
    return out;
}
function relToSlug(rel) {
    return rel.replace(/\.md$/, "");
}
function compareDocs(a, b) {
    if (a.frontmatter.order !== b.frontmatter.order) {
        return a.frontmatter.order - b.frontmatter.order;
    }
    return compareStable(a.frontmatter.title, b.frontmatter.title);
}
export function buildDocsIndex(opts) {
    const { docsRoot, manifest, env, manifestEntries, includeDeprecated } = opts;
    const files = collectMarkdownFiles(docsRoot);
    // Resolve all entries up front (single pass).
    const all = [];
    for (const rel of files) {
        const abs = path.join(docsRoot, rel);
        let source = "";
        try {
            source = fs.readFileSync(abs, "utf-8");
        }
        catch {
            continue;
        }
        const entry = manifestEntries?.get(`docs/${rel}`);
        const frontmatter = resolveFrontmatter(rel, source, entry);
        if (!isVisible(frontmatter, env))
            continue;
        if (frontmatter.deprecated && !includeDeprecated)
            continue;
        all.push({
            slug: relToSlug(rel),
            repo_path: `docs/${rel}`,
            frontmatter,
        });
    }
    // Index by (category, subcategory).
    const byCategory = new Map();
    for (const doc of all) {
        const catKey = doc.frontmatter.category;
        const subKey = doc.frontmatter.subcategory;
        let bySub = byCategory.get(catKey);
        if (!bySub) {
            bySub = new Map();
            byCategory.set(catKey, bySub);
        }
        let list = bySub.get(subKey);
        if (!list) {
            list = [];
            bySub.set(subKey, list);
        }
        list.push(doc);
    }
    // Build groups using manifest-declared display ordering. Categories not
    // declared in the manifest are appended in alphabetical order so newly
    // added docs are still discoverable.
    const seenCatKeys = new Set();
    const categories = [];
    for (const cat of manifest.categories) {
        seenCatKeys.add(cat.key);
        const bySub = byCategory.get(cat.key);
        if (!bySub)
            continue;
        const subSeen = new Set();
        const subcategories = [];
        for (const sub of cat.subcategories) {
            subSeen.add(sub.key);
            const docs = bySub.get(sub.key);
            if (!docs || docs.length === 0)
                continue;
            docs.sort(compareDocs);
            subcategories.push({
                key: sub.key,
                display_name: sub.display_name,
                description: sub.description ?? null,
                order: sub.order,
                docs,
            });
        }
        // Append any undeclared subcategories alphabetically.
        const extraSubKeys = [];
        for (const k of bySub.keys()) {
            if (k && !subSeen.has(k))
                extraSubKeys.push(k);
        }
        extraSubKeys.sort(compareStable);
        for (const k of extraSubKeys) {
            const docs = bySub.get(k);
            docs.sort(compareDocs);
            subcategories.push({
                key: k,
                display_name: humanizeKey(k),
                description: null,
                order: 1000,
                docs,
            });
        }
        const uncategorized = (bySub.get(null) ?? []).slice().sort(compareDocs);
        categories.push({
            key: cat.key,
            display_name: cat.display_name,
            description: cat.description ?? null,
            order: cat.order,
            subcategories,
            uncategorized,
        });
    }
    // Append undeclared categories.
    const extraCatKeys = [];
    for (const k of byCategory.keys()) {
        if (!seenCatKeys.has(k))
            extraCatKeys.push(k);
    }
    extraCatKeys.sort(compareStable);
    for (const k of extraCatKeys) {
        const bySub = byCategory.get(k);
        const subKeys = [];
        for (const sk of bySub.keys())
            if (sk)
                subKeys.push(sk);
        subKeys.sort();
        const subcategories = subKeys.map((sk) => {
            const docs = bySub.get(sk).slice().sort(compareDocs);
            return {
                key: sk,
                display_name: humanizeKey(sk),
                description: null,
                order: 1000,
                docs,
            };
        });
        const uncategorized = (bySub.get(null) ?? []).slice().sort(compareDocs);
        categories.push({
            key: k,
            display_name: humanizeKey(k),
            description: null,
            order: 1000,
            subcategories,
            uncategorized,
        });
    }
    // Featured: manifest order, filtered by visibility.
    const bySlug = new Map(all.map((d) => [d.repo_path, d]));
    const featured = [];
    for (const repoPath of manifest.featured) {
        const entry = bySlug.get(repoPath);
        if (entry)
            featured.push(entry);
    }
    return { categories, featured, total: all.length };
}
function humanizeKey(key) {
    return key.replace(/[_-]+/g, " ").replace(/\b([a-z])/g, (_, c) => c.toUpperCase());
}
//# sourceMappingURL=index_builder.js.map