/**
 * Derive a bundle's catalog by scanning its content facet directories.
 *
 * Entry point for the pluggable-bundle catalog subsystem. Called from each
 * bundle's top-level `index.ts` (e.g. `entities/src/core/models/index.ts`)
 * to produce a `CatalogEntry[]` that replaces hand-maintained `docs/index.ts`
 * + `tests/index.ts` arrays.
 *
 * Results are cached per bundle directory — safe to call from module-load
 * paths; subsequent calls for the same bundle return the cached array.
 *
 * See: specverse-self/docs/proposals/PLUGGABLE-BUNDLE-MODEL.md
 */
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
import { basename, dirname, extname, join, relative, resolve } from 'path';
import { DEFAULT_FACETS, } from './types.js';
const cache = new Map();
export function deriveCatalog(bundleDir, options = {}) {
    // Normalise to absolute so cache hits work across relative callers.
    const root = resolve(bundleDir);
    const cacheKey = JSON.stringify({
        root,
        additional: options.additionalReferences,
        facets: options.facets,
    });
    const cached = cache.get(cacheKey);
    if (cached)
        return cached;
    const facets = {
        ...DEFAULT_FACETS,
        ...(options.facets ?? {}),
    };
    const entries = [];
    for (const facet of Object.keys(facets)) {
        const facetDir = join(root, facet);
        if (!existsSync(facetDir) || !statSync(facetDir).isDirectory())
            continue;
        const pattern = facets[facet];
        const regex = globToRegExp(pattern.glob);
        for (const abs of walkFiles(facetDir)) {
            const relFromFacet = relative(facetDir, abs);
            if (!regex.test(relFromFacet))
                continue;
            const entry = buildEntry(abs, root, pattern.category);
            entries.push(entry);
        }
    }
    // Mixed mode: caller-supplied external references appended after derived.
    if (options.additionalReferences) {
        entries.push(...options.additionalReferences);
    }
    cache.set(cacheKey, entries);
    return entries;
}
/** Clear the cache — primarily for tests that mutate fixture content between runs. */
export function clearCatalogCache() {
    cache.clear();
}
// ── Title / description extraction ────────────────────────────────────────
function buildEntry(filePath, bundleRoot, category) {
    const relPath = './' + relative(bundleRoot, filePath).replaceAll('\\', '/');
    const ext = extname(filePath).toLowerCase();
    let title = '';
    let description;
    if (ext === '.specly') {
        // Look for colocated `<basename>.md` as the doc.
        const mdPath = join(dirname(filePath), basename(filePath, '.specly') + '.md');
        if (existsSync(mdPath)) {
            const md = readMdLeading(mdPath);
            title = md.title;
            description = md.description;
        }
        if (!title)
            title = humanize(basename(filePath, '.specly'));
    }
    else if (ext === '.qnt') {
        // Leading `//` comment block.
        const c = readQuintLeading(filePath);
        title = c.title || humanize(basename(filePath, '.qnt'));
        description = c.description;
    }
    else if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.mjs') {
        // Test file — extract leading JSDoc-style comment.
        const c = readJSDocLeading(filePath);
        title = c.title || humanize(basename(filePath).replace(/\.(test|spec)\.\w+$/, ''));
        description = c.description;
    }
    else {
        title = humanize(basename(filePath, ext));
    }
    const entry = { title, category, path: relPath };
    if (description)
        entry.description = description;
    return entry;
}
function readMdLeading(mdPath) {
    let content;
    try {
        content = readFileSync(mdPath, 'utf8');
    }
    catch {
        return { title: '' };
    }
    const lines = content.split('\n');
    let title = '';
    let description;
    let seenH1 = false;
    for (const line of lines) {
        if (!seenH1) {
            const h1 = line.match(/^#\s+(.+)$/);
            if (h1) {
                title = h1[1].trim();
                seenH1 = true;
                continue;
            }
            continue;
        }
        const trimmed = line.trim();
        if (!trimmed)
            continue;
        if (trimmed.startsWith('#'))
            break; // next heading — description ends
        if (trimmed.startsWith('```'))
            break; // code fence — not prose
        if (trimmed.startsWith('>'))
            continue; // blockquote — skip
        description = trimmed.replace(/^\*\*|\*\*$/g, '').replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
        break;
    }
    return { title, description };
}
function readQuintLeading(qntPath) {
    let content;
    try {
        content = readFileSync(qntPath, 'utf8');
    }
    catch {
        return { title: '' };
    }
    const commentLines = [];
    for (const line of content.split('\n')) {
        const t = line.trim();
        if (t.startsWith('//')) {
            commentLines.push(t.replace(/^\/\/\s?/, ''));
            continue;
        }
        if (t === '') {
            if (commentLines.length)
                continue;
            continue;
        }
        break; // first non-comment, non-blank line ends the prolog
    }
    const [titleLine, ...rest] = commentLines.filter(Boolean);
    return {
        title: (titleLine ?? '').trim(),
        description: rest.length ? rest.join(' ').trim() : undefined,
    };
}
function readJSDocLeading(filePath) {
    let content;
    try {
        content = readFileSync(filePath, 'utf8');
    }
    catch {
        return { title: '' };
    }
    const match = content.match(/^\s*\/\*\*([\s\S]*?)\*\//);
    if (!match)
        return { title: '' };
    const lines = match[1]
        .split('\n')
        .map((l) => l.replace(/^\s*\*\s?/, '').trim())
        .filter((l) => !l.startsWith('@'));
    const nonEmpty = lines.filter(Boolean);
    const [titleLine, ...rest] = nonEmpty;
    return {
        title: (titleLine ?? '').trim(),
        description: rest.length ? rest.join(' ').trim() : undefined,
    };
}
function humanize(slug) {
    return slug
        .replace(/[-_]+/g, ' ')
        .replace(/\b\w/g, (c) => c.toUpperCase());
}
// ── Filesystem walking + glob matching ────────────────────────────────────
function* walkFiles(dir) {
    let entries;
    try {
        entries = readdirSync(dir);
    }
    catch {
        return;
    }
    for (const entry of entries) {
        const full = join(dir, entry);
        let s;
        try {
            s = statSync(full);
        }
        catch {
            continue;
        }
        if (s.isDirectory())
            yield* walkFiles(full);
        else if (s.isFile())
            yield full;
    }
}
/**
 * Minimal glob → RegExp for the patterns we support:
 *   `**` → any path segments (including none)
 *   `*`  → non-separator run
 *   `{a,b}` → alternation
 * Paths use forward slashes for matching regardless of OS.
 */
function globToRegExp(glob) {
    let re = '';
    let i = 0;
    while (i < glob.length) {
        const ch = glob[i];
        if (ch === '*' && glob[i + 1] === '*') {
            // `**` or `**/`
            if (glob[i + 2] === '/') {
                re += '(?:.*/)?';
                i += 3;
            }
            else {
                re += '.*';
                i += 2;
            }
        }
        else if (ch === '*') {
            re += '[^/]*';
            i += 1;
        }
        else if (ch === '{') {
            const end = glob.indexOf('}', i);
            if (end === -1) {
                re += '\\{';
                i += 1;
                continue;
            }
            const alts = glob.slice(i + 1, end).split(',').map(escapeRegex);
            re += '(?:' + alts.join('|') + ')';
            i = end + 1;
        }
        else if ('/.?+^$()[]\\'.includes(ch)) {
            re += '\\' + ch;
            i += 1;
        }
        else {
            re += ch;
            i += 1;
        }
    }
    return new RegExp('^' + re + '$');
}
function escapeRegex(s) {
    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
//# sourceMappingURL=deriveCatalog.js.map