/**
 * Tool Registry
 *
 * Central registry for all assessment tools. Manages tool metadata, visibility logic,
 * and button/instance creation. Supports dynamic registration and override by integrators.
 */
import { normalizeToolAlias } from "./tools-config-normalizer.js";
const VALID_TOOL_LEVELS = [
    "assessment",
    "section",
    "item",
    "passage",
    "rubric",
    "element",
];
function assertNonEmptyString(value, fieldName) {
    if (typeof value !== "string" || value.trim().length === 0) {
        throw new Error(`Invalid tool registration: "${fieldName}" must be a non-empty string.`);
    }
}
// Defence-in-depth: reject obvious XSS payloads in tool-registered icon
// markup at registration time. Runtime rendering still runs each icon
// through DOMPurify (see `ToolIcon.svelte`), but surfacing the problem
// early produces a clearer error for tool authors than "the icon silently
// disappeared after sanitization".
const SCRIPTABLE_ICON_PATTERNS = [
    { pattern: /<script\b/i, reason: "contains a <script> tag" },
    {
        pattern: /\son[a-z]+\s*=/i,
        reason: "contains an inline event handler (on*=) attribute",
    },
    { pattern: /javascript:/i, reason: "contains a javascript: URL" },
    {
        pattern: /<foreignObject\b/i,
        reason: "contains a <foreignObject> element",
    },
];
function assertIconStringIsSafe(toolId, icon, fieldName) {
    const trimmed = icon.trimStart();
    const looksLikeSvg = trimmed.toLowerCase().startsWith("<svg");
    const looksLikeUrl = /^https?:/i.test(trimmed);
    const looksLikeDataUrl = /^data:/i.test(trimmed);
    if (looksLikeDataUrl) {
        throw new Error(`Invalid tool registration "${toolId}": "${fieldName}" may not be a data: URL.`);
    }
    if (!looksLikeSvg && !looksLikeUrl)
        return;
    for (const { pattern, reason } of SCRIPTABLE_ICON_PATTERNS) {
        if (pattern.test(icon)) {
            throw new Error(`Invalid tool registration "${toolId}": "${fieldName}" ${reason}. Inline SVG icons must not include scriptable content.`);
        }
    }
}
function assertToolRegistrationShape(registration) {
    assertNonEmptyString(registration.toolId, "toolId");
    assertNonEmptyString(registration.name, "name");
    assertNonEmptyString(registration.description, "description");
    if (typeof registration.icon !== "string" &&
        typeof registration.icon !== "function") {
        throw new Error(`Invalid tool registration "${registration.toolId}": "icon" must be a string or function.`);
    }
    if (typeof registration.icon === "string") {
        assertIconStringIsSafe(registration.toolId, registration.icon, "icon");
    }
    if (!Array.isArray(registration.supportedLevels) ||
        registration.supportedLevels.length === 0) {
        throw new Error(`Invalid tool registration "${registration.toolId}": "supportedLevels" must be a non-empty array.`);
    }
    const invalidLevel = registration.supportedLevels.find((level) => !VALID_TOOL_LEVELS.includes(level));
    if (invalidLevel) {
        throw new Error(`Invalid tool registration "${registration.toolId}": unsupported level "${invalidLevel}".`);
    }
    if (registration.activation !== undefined &&
        registration.activation !== "toolbar-toggle" &&
        registration.activation !== "selection-gateway") {
        throw new Error(`Invalid tool registration "${registration.toolId}": unsupported activation "${String(registration.activation)}".`);
    }
    if (registration.singletonScope !== undefined &&
        registration.singletonScope !== "section") {
        throw new Error(`Invalid tool registration "${registration.toolId}": unsupported singletonScope "${String(registration.singletonScope)}".`);
    }
    if (registration.activation === "selection-gateway" &&
        registration.singletonScope !== "section") {
        throw new Error(`Invalid tool registration "${registration.toolId}": selection-gateway tools must declare singletonScope "section".`);
    }
    if (registration.pnpSupportIds !== undefined &&
        (!Array.isArray(registration.pnpSupportIds) ||
            registration.pnpSupportIds.some((pnpId) => typeof pnpId !== "string" || pnpId.trim().length === 0))) {
        throw new Error(`Invalid tool registration "${registration.toolId}": "pnpSupportIds" must be an array of non-empty strings.`);
    }
    if (typeof registration.isVisibleInContext !== "function") {
        throw new Error(`Invalid tool registration "${registration.toolId}": "isVisibleInContext" must be a function.`);
    }
    if (typeof registration.renderToolbar !== "function") {
        throw new Error(`Invalid tool registration "${registration.toolId}": "renderToolbar" must be a function.`);
    }
}
/**
 * Tool Registry
 *
 * Manages tool registrations and provides query/lookup functionality
 */
export class ToolRegistry {
    tools = new Map();
    pnpIndex = new Map(); // pnpSupportId → Set<toolId>
    componentOverrides = {};
    moduleLoaders = new Map();
    loadedToolModules = new Set();
    moduleLoadPromises = new Map();
    /**
     * Normalize a single tool alias to canonical toolId.
     */
    normalizeToolId(toolId) {
        return normalizeToolAlias(toolId);
    }
    /**
     * Normalize a list of tool aliases to canonical toolIds.
     */
    normalizeToolIds(toolIds) {
        return toolIds.map((toolId) => this.normalizeToolId(toolId));
    }
    /**
     * Register a tool
     *
     * @param registration - Tool registration
     * @throws Error if toolId is already registered
     */
    register(registration) {
        assertToolRegistrationShape(registration);
        if (this.tools.has(registration.toolId)) {
            throw new Error(`Tool '${registration.toolId}' is already registered`);
        }
        this.tools.set(registration.toolId, registration);
        // Index PNP support IDs
        if (registration.pnpSupportIds) {
            for (const pnpId of registration.pnpSupportIds) {
                if (!this.pnpIndex.has(pnpId)) {
                    this.pnpIndex.set(pnpId, new Set());
                }
                this.pnpIndex.get(pnpId).add(registration.toolId);
            }
        }
    }
    /**
     * Override an existing tool registration
     *
     * @param registration - New tool registration (must have existing toolId)
     */
    override(registration) {
        assertToolRegistrationShape(registration);
        if (!this.tools.has(registration.toolId)) {
            throw new Error(`Cannot override non-existent tool '${registration.toolId}'`);
        }
        // Remove old PNP index entries
        const oldReg = this.tools.get(registration.toolId);
        if (oldReg.pnpSupportIds) {
            for (const pnpId of oldReg.pnpSupportIds) {
                this.pnpIndex.get(pnpId)?.delete(registration.toolId);
            }
        }
        // Add new registration
        this.tools.set(registration.toolId, registration);
        // Re-index PNP support IDs
        if (registration.pnpSupportIds) {
            for (const pnpId of registration.pnpSupportIds) {
                if (!this.pnpIndex.has(pnpId)) {
                    this.pnpIndex.set(pnpId, new Set());
                }
                this.pnpIndex.get(pnpId).add(registration.toolId);
            }
        }
    }
    /**
     * Unregister a tool
     *
     * @param toolId - Tool ID to remove
     */
    unregister(toolId) {
        const reg = this.tools.get(toolId);
        if (!reg)
            return;
        // Remove PNP index entries
        if (reg.pnpSupportIds) {
            for (const pnpId of reg.pnpSupportIds) {
                this.pnpIndex.get(pnpId)?.delete(toolId);
            }
        }
        this.tools.delete(toolId);
    }
    /**
     * Get a tool registration by ID
     *
     * @param toolId - Tool ID
     * @returns Tool registration or undefined
     */
    get(toolId) {
        return this.tools.get(toolId);
    }
    /**
     * Check if a tool is registered
     *
     * @param toolId - Tool ID
     * @returns true if registered
     */
    has(toolId) {
        return this.tools.has(toolId);
    }
    /**
     * Get all registered tool IDs
     *
     * @returns Array of tool IDs
     */
    getAllToolIds() {
        return Array.from(this.tools.keys());
    }
    /**
     * Get all tool registrations
     *
     * @returns Array of tool registrations
     */
    getAllTools() {
        return Array.from(this.tools.values());
    }
    /**
     * Find tool IDs that support a given PNP support ID
     *
     * @param pnpSupportId - PNP support ID (e.g., 'calculator')
     * @returns Set of tool IDs that support this PNP ID
     */
    getToolsByPNPSupport(pnpSupportId) {
        return this.pnpIndex.get(pnpSupportId) || new Set();
    }
    /**
     * Get tools that support a specific level
     *
     * @param level - Tool level (assessment, section, item, passage, element)
     * @returns Array of tool registrations that support this level
     */
    getToolsByLevel(level) {
        return this.getAllTools().filter((tool) => tool.supportedLevels.includes(level));
    }
    /**
     * Resolve tool activation, defaulting to toolbar-toggle.
     */
    getToolActivation(toolId) {
        return this.get(toolId)?.activation || "toolbar-toggle";
    }
    /**
     * Resolve singleton scope for a tool when present.
     */
    getToolSingletonScope(toolId) {
        return this.get(toolId)?.singletonScope || null;
    }
    /**
     * Filter tool IDs by activation type.
     */
    filterToolIdsByActivation(toolIds, activation) {
        return toolIds.filter((toolId) => this.getToolActivation(toolId) === activation);
    }
    /**
     * Filter tools by visibility in a given context
     *
     * Pass 2 of the two-pass model: Given a list of allowed tool IDs (from Pass 1),
     * ask each tool if it's relevant in this context.
     *
     * @param allowedToolIds - Tool IDs that passed Pass 1 (orchestrator approval)
     * @param context - Context to evaluate
     * @returns Array of visible tool registrations
     */
    filterVisibleInContext(allowedToolIds, context) {
        const visible = [];
        for (const toolId of allowedToolIds) {
            const tool = this.get(toolId);
            if (!tool) {
                console.warn(`Tool '${toolId}' is allowed but not registered`);
                continue;
            }
            // Check if tool supports this level
            if (!tool.supportedLevels.includes(context.level)) {
                continue;
            }
            // Pass 2: Ask tool if it's relevant
            try {
                if (tool.isVisibleInContext(context)) {
                    visible.push(tool);
                }
            }
            catch (error) {
                console.error(`Error evaluating visibility for tool '${toolId}':`, error);
            }
        }
        return visible;
    }
    /**
     * Get tool metadata for building UIs
     * Useful for building PNP configuration interfaces
     *
     * @returns Array of tool metadata (id, name, description, pnpSupportIds)
     */
    getToolMetadata() {
        return this.getAllTools().map((tool) => ({
            toolId: tool.toolId,
            name: tool.name,
            description: tool.description,
            pnpSupportIds: tool.pnpSupportIds || [],
            supportedLevels: tool.supportedLevels,
            activation: tool.activation || "toolbar-toggle",
            singletonScope: tool.singletonScope || null,
        }));
    }
    /**
     * Generate PNP support IDs from enabled tools
     * Useful for creating PNP profiles
     *
     * @param enabledToolIds - Tool IDs to enable
     * @returns Array of unique PNP support IDs
     */
    generatePNPSupportsFromTools(enabledToolIds) {
        const pnpSupports = new Set();
        for (const toolId of enabledToolIds) {
            const tool = this.get(toolId);
            if (tool?.pnpSupportIds) {
                for (const pnpId of tool.pnpSupportIds) {
                    pnpSupports.add(pnpId);
                }
            }
        }
        return Array.from(pnpSupports);
    }
    /**
     * Clear all registrations (useful for testing)
     */
    clear() {
        this.tools.clear();
        this.pnpIndex.clear();
    }
    /**
     * Configure global component overrides used by tool instance creation.
     */
    setComponentOverrides(overrides) {
        this.componentOverrides = overrides;
    }
    /**
     * Register lazy module loaders by toolId.
     * Toolbars call ensureToolModuleLoaded(toolId) before instance creation.
     */
    setToolModuleLoaders(loaders) {
        for (const [toolId, loader] of Object.entries(loaders)) {
            if (!loader)
                continue;
            assertNonEmptyString(toolId, "tool module loader id");
            if (typeof loader !== "function") {
                throw new Error(`Invalid tool module loader for "${toolId}": expected a function.`);
            }
            this.moduleLoaders.set(toolId, loader);
        }
    }
    /**
     * Ensure tool module side-effects are loaded exactly once.
     * Safe to call repeatedly; concurrent callers share the same promise.
     */
    async ensureToolModuleLoaded(toolId) {
        if (this.loadedToolModules.has(toolId))
            return;
        const existingPromise = this.moduleLoadPromises.get(toolId);
        if (existingPromise) {
            await existingPromise;
            return;
        }
        const loader = this.moduleLoaders.get(toolId);
        if (!loader)
            return;
        const loadPromise = (async () => {
            await loader();
            this.loadedToolModules.add(toolId);
        })();
        this.moduleLoadPromises.set(toolId, loadPromise);
        try {
            await loadPromise;
        }
        finally {
            this.moduleLoadPromises.delete(toolId);
        }
    }
    /**
     * Ensure a set of tool modules are loaded.
     */
    async ensureToolModulesLoaded(toolIds) {
        await Promise.all(toolIds.map((toolId) => this.ensureToolModuleLoaded(toolId)));
    }
    /**
     * Whether a tool module has already been loaded.
     */
    isToolModuleLoaded(toolId) {
        return this.loadedToolModules.has(toolId);
    }
    /**
     * Render a tool for toolbar use with component overrides attached.
     */
    renderForToolbar(toolId, context, toolbarContext) {
        const tool = this.get(toolId);
        if (!tool) {
            throw new Error(`Tool '${toolId}' is not registered`);
        }
        const mergedContext = {
            ...toolbarContext,
            componentOverrides: {
                ...(this.componentOverrides || {}),
                ...(toolbarContext.componentOverrides || {}),
            },
        };
        return tool.renderToolbar(context, mergedContext);
    }
}
//# sourceMappingURL=ToolRegistry.js.map