// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// engine/plugin-installer/plugin-catalog.ts — Load and query plugin recommendations (SPEC-596)
//
// Reads src/config/plugin-recommendations.json and exposes getRecommendedForStack().
// Language and stack agnostic: catalog drives everything.
import { readFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { z } from 'zod';
import { PluginRecommendationSchema, } from '../../types/plugin-install.js';
const CatalogSchema = z.object({
    plugins: z.array(PluginRecommendationSchema),
});
// Resolve catalog path relative to this compiled file (works in both src/ and dist/)
function catalogPath() {
    const currentDir = dirname(fileURLToPath(import.meta.url));
    return join(currentDir, '../../config/plugin-recommendations.json');
}
/** Load and validate the plugin catalog from disk. Cached per process lifetime. */
let catalogCache = null;
export async function loadCatalog() {
    if (catalogCache !== null) {
        return catalogCache;
    }
    const raw = await readFile(catalogPath(), 'utf-8');
    const parsed = JSON.parse(raw);
    catalogCache = CatalogSchema.parse(parsed);
    return catalogCache;
}
/** Expose for tests that need to inject a catalog without touching the file system. */
export function setCatalogCache(catalog) {
    catalogCache = catalog;
}
/**
 * Search all plugins in the catalog by keyword and/or stack filter.
 *
 * - No filters: returns all plugins (deduplicated across tiers).
 * - keyword: case-insensitive match against id, displayName, or description.
 * - stack: case-insensitive match against the plugin's stacks array.
 * - Both filters apply with AND logic.
 */
export async function searchPlugins(keyword, stack) {
    const catalog = await loadCatalog();
    const seen = new Set();
    const results = [];
    for (const plugin of catalog.plugins) {
        if (seen.has(plugin.id)) {
            continue;
        }
        if (keyword !== undefined) {
            const kw = keyword.toLowerCase();
            const matchesKeyword = plugin.id.toLowerCase().includes(kw) ||
                plugin.displayName.toLowerCase().includes(kw) ||
                plugin.description.toLowerCase().includes(kw);
            if (!matchesKeyword) {
                continue;
            }
        }
        if (stack !== undefined) {
            const stackLower = stack.toLowerCase();
            const matchesStack = plugin.stacks.some((s) => s.toLowerCase().includes(stackLower));
            if (!matchesStack) {
                continue;
            }
        }
        seen.add(plugin.id);
        results.push(plugin);
    }
    return results;
}
/**
 * Return recommended plugins for the given stack signals and install mode.
 *
 * - 'minimal': always-tier plugins only
 * - 'recommended': always-tier + stack-matched plugins
 * - 'full': everything in the catalog that matches (always + stack-matched + optional)
 */
export async function getRecommendedForStack(signals, mode) {
    const catalog = await loadCatalog();
    const signalNames = new Set(signals.map((s) => s.name.toLowerCase()));
    return catalog.plugins.filter((plugin) => {
        if (plugin.tier === 'always') {
            return true;
        }
        if (mode === 'minimal') {
            return false;
        }
        if (plugin.tier === 'stack-matched') {
            return plugin.stacks.some((s) => signalNames.has(s.toLowerCase()));
        }
        // tier === 'optional': included in 'full' mode only
        if (mode === 'full') {
            return (plugin.stacks.length === 0 || plugin.stacks.some((s) => signalNames.has(s.toLowerCase())));
        }
        return false;
    });
}
//# sourceMappingURL=plugin-catalog.js.map