Source: compose/sparql/TemplateManager.js

/**
 * TemplateManager - Manage SPARQL query templates with caching and parameterization
 * 
 * This component provides centralized management of SPARQL query templates with:
 * 1. Template loading and caching
 * 2. Parameter substitution and validation
 * 3. Common prefix management
 * 4. Query optimization
 * 
 * API: getQuery(templateName, parameters, options)
 */

import logger from 'loglevel';

export default class TemplateManager {
    constructor(options = {}) {
        this.options = {
            cacheTemplates: options.cacheTemplates !== false, // Default to true
            validateParameters: options.validateParameters !== false, // Default to true
            ...options
        };
        
        this.templateCache = new Map();
        this.prefixCache = new Map();
        
        // Initialize common prefixes
        this._initializePrefixes();
    }

    /**
     * Get a SPARQL query from template with parameter substitution
     * 
     * @param {string} templateName - Name of the template to use
     * @param {Object} parameters - Parameters for template substitution
     * @param {Object} options - Query generation options
     * @param {boolean} options.includePrefixes - Include prefix declarations (default: true)
     * @param {Array<string>} options.additionalPrefixes - Additional prefixes to include
     * @param {boolean} options.optimize - Optimize query structure (default: false)
     * @returns {Object} Generated query with metadata
     */
    async getQuery(templateName, parameters = {}, options = {}) {
        try {
            const queryOptions = {
                includePrefixes: options.includePrefixes !== false,
                additionalPrefixes: options.additionalPrefixes || [],
                optimize: options.optimize || false,
                ...options
            };

            // Get template
            const template = await this._getTemplate(templateName);
            if (!template) {
                throw new Error(`Template not found: ${templateName}`);
            }

            // Validate parameters if enabled
            if (this.options.validateParameters) {
                this._validateParameters(template, parameters);
            }

            // Substitute parameters
            let query = this._substituteParameters(template.query, parameters);

            // Add prefixes if requested
            if (queryOptions.includePrefixes) {
                const prefixes = this._buildPrefixes(template.prefixes, queryOptions.additionalPrefixes);
                query = prefixes + '\n\n' + query;
            }

            // Optimize query if requested
            if (queryOptions.optimize) {
                query = this._optimizeQuery(query);
            }

            return {
                success: true,
                query: query.trim(),
                metadata: {
                    templateName,
                    parametersUsed: Object.keys(parameters),
                    prefixesIncluded: queryOptions.includePrefixes,
                    queryLength: query.length,
                    timestamp: new Date().toISOString()
                }
            };

        } catch (error) {
            logger.error('Failed to generate query from template:', error.message);
            return {
                success: false,
                error: error.message,
                query: null,
                metadata: {
                    templateName,
                    errorOccurred: true,
                    timestamp: new Date().toISOString()
                }
            };
        }
    }

    /**
     * Register a new template
     * 
     * @param {string} name - Template name
     * @param {Object} template - Template definition
     * @param {string} template.query - Query template with placeholders
     * @param {Array<string>} template.prefixes - Required prefixes
     * @param {Array<string>} template.parameters - Required parameters
     * @param {string} template.description - Template description
     * @returns {boolean} Success status
     */
    registerTemplate(name, template) {
        try {
            // Validate template structure
            if (!template.query || typeof template.query !== 'string') {
                throw new Error('Template must have a query string');
            }

            const templateDef = {
                query: template.query,
                prefixes: template.prefixes || ['ragno', 'rdf', 'rdfs'],
                parameters: template.parameters || [],
                description: template.description || '',
                registeredAt: new Date().toISOString()
            };

            this.templateCache.set(name, templateDef);
            return true;

        } catch (error) {
            logger.error(`Failed to register template ${name}:`, error.message);
            return false;
        }
    }

    /**
     * List available templates
     * 
     * @returns {Array<Object>} List of available templates with metadata
     */
    listTemplates() {
        const templates = [];
        
        for (const [name, template] of this.templateCache.entries()) {
            templates.push({
                name,
                description: template.description,
                parameters: template.parameters,
                prefixes: template.prefixes,
                registeredAt: template.registeredAt
            });
        }

        return templates.sort((a, b) => a.name.localeCompare(b.name));
    }

    /**
     * Clear template cache
     */
    clearCache() {
        this.templateCache.clear();
        logger.debug('Template cache cleared');
    }

    /**
     * Get template from cache or load it
     * @private
     */
    async _getTemplate(templateName) {
        // Check cache first
        if (this.templateCache.has(templateName)) {
            return this.templateCache.get(templateName);
        }

        // Try to load built-in template
        const builtInTemplate = this._getBuiltInTemplate(templateName);
        if (builtInTemplate) {
            this.templateCache.set(templateName, builtInTemplate);
            return builtInTemplate;
        }

        return null;
    }

    /**
     * Get built-in template definitions
     * @private
     */
    _getBuiltInTemplate(templateName) {
        const builtInTemplates = {
            // INSERT operations
            'insert-data': {
                query: `INSERT DATA {
    GRAPH <\${graph}> {
        \${triples}
    }
}`,
                prefixes: ['ragno', 'rdf', 'rdfs', 'xsd', 'dcterms', 'prov'],
                parameters: ['graph', 'triples'],
                description: 'Insert RDF triples into a named graph'
            },

            'insert-corpuscle': {
                query: `INSERT DATA {
    GRAPH <\${graph}> {
        <\${uri}> a ragno:Corpuscle ;
                  rdfs:label "\${label}" ;
                  dcterms:created "\${timestamp}" .
        \${additionalTriples}
    }
}`,
                prefixes: ['ragno', 'rdfs', 'dcterms'],
                parameters: ['graph', 'uri', 'label', 'timestamp'],
                description: 'Insert a new ragno:Corpuscle with basic properties'
            },

            // SELECT operations
            'select-questions-with-relationships': {
                query: `SELECT ?question ?questionText ?relationship ?targetEntity ?relationshipType ?weight ?sourceCorpus
WHERE {
    GRAPH <\${graph}> {
        ?question a ragno:Corpuscle ;
                 rdfs:label ?questionText .
        
        ?relationship a ragno:Relationship ;
                     ragno:hasSourceEntity ?question ;
                     ragno:hasTargetEntity ?targetEntity ;
                     ragno:relationshipType ?relationshipType ;
                     ragno:weight ?weight .
        
        OPTIONAL { ?relationship ragno:sourceCorpus ?sourceCorpus }
        
        FILTER(?question != ?targetEntity)
        \${additionalFilters}
    }
}
ORDER BY ?question DESC(?weight)
\${limitClause}`,
                prefixes: ['ragno', 'rdfs'],
                parameters: ['graph'],
                description: 'Select questions with their relationships and target entities'
            },

            'select-entities-by-uris': {
                query: `SELECT ?entity ?content
WHERE {
    GRAPH <\${graph}> {
        ?entity a ragno:Corpuscle ;
               rdfs:label ?content .
        
        FILTER(?entity IN (\${entityList}))
        \${additionalFilters}
    }
}
\${orderClause}
\${limitClause}`,
                prefixes: ['ragno', 'rdfs'],
                parameters: ['graph', 'entityList'],
                description: 'Select entities by a list of URIs'
            },

            'select-navigable-questions': {
                query: `SELECT ?question ?questionText ?embedding ?conceptValue ?conceptType ?conceptConfidence
WHERE {
    GRAPH <\${graph}> {
        ?question a ragno:Corpuscle ;
                 rdfs:label ?questionText .
        
        # Must have embedding for similarity search
        ?question ragno:hasAttribute ?embeddingAttr .
        {
            ?embeddingAttr a ragno:VectorEmbedding ;
                          ragno:attributeValue ?embedding .
        } UNION {
            ?embeddingAttr ragno:attributeType "vector-embedding" ;
                          ragno:attributeValue ?embedding .
        }
        
        # Must have concepts for semantic navigation
        ?question ragno:hasAttribute ?attr .
        ?attr ragno:attributeType "concept" ;
              ragno:attributeValue ?conceptValue .
        
        OPTIONAL { ?attr ragno:attributeConfidence ?conceptConfidence }
        OPTIONAL { ?attr ragno:attributeSubType ?conceptType }
        
        \${additionalFilters}
    }
}
ORDER BY ?question
\${limitClause}`,
                prefixes: ['ragno', 'rdfs'],
                parameters: ['graph'],
                description: 'Select questions with embeddings and concepts for navigation'
            },

            'select-test-questions': {
                query: `SELECT ?corpuscle ?label ?source WHERE {
    GRAPH <\${graph}> {
        ?corpuscle a ragno:Corpuscle ;
                 rdfs:label ?label ;
                 dcterms:source ?source ;
                 ragno:corpuscleType "test-question" .
        \${additionalFilters}
    }
}
ORDER BY ?corpuscle
\${limitClause}`,
                prefixes: ['ragno', 'rdfs', 'dcterms'],
                parameters: ['graph'],
                description: 'Select test questions from the knowledge base'
            },

            // Graph management
            'clear-graph': {
                query: `CLEAR GRAPH <\${graph}>`,
                prefixes: [],
                parameters: ['graph'],
                description: 'Clear all triples from a named graph'
            },

            'drop-graph': {
                query: `DROP GRAPH <\${graph}>`,
                prefixes: [],
                parameters: ['graph'],
                description: 'Drop a named graph completely'
            }
        };

        return builtInTemplates[templateName] || null;
    }

    /**
     * Initialize common prefix definitions
     * @private
     */
    _initializePrefixes() {
        const commonPrefixes = {
            ragno: 'http://purl.org/stuff/ragno/',
            rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
            rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
            xsd: 'http://www.w3.org/2001/XMLSchema#',
            dcterms: 'http://purl.org/dc/terms/',
            prov: 'http://www.w3.org/ns/prov#',
            owl: 'http://www.w3.org/2002/07/owl#',
            beerqa: 'http://purl.org/stuff/beerqa/',
            wikidata: 'http://www.wikidata.org/entity/',
            wd: 'http://www.wikidata.org/entity/',
            wdt: 'http://www.wikidata.org/prop/direct/',
            wikipedia: 'https://en.wikipedia.org/wiki/'
        };

        for (const [prefix, uri] of Object.entries(commonPrefixes)) {
            this.prefixCache.set(prefix, uri);
        }
    }

    /**
     * Build prefix declarations
     * @private
     */
    _buildPrefixes(requiredPrefixes, additionalPrefixes = []) {
        const allPrefixes = [...new Set([...requiredPrefixes, ...additionalPrefixes])];
        const prefixLines = [];

        for (const prefix of allPrefixes) {
            if (this.prefixCache.has(prefix)) {
                prefixLines.push(`PREFIX ${prefix}: <${this.prefixCache.get(prefix)}>`);
            } else {
                logger.warn(`Unknown prefix: ${prefix}`);
            }
        }

        return prefixLines.join('\n');
    }

    /**
     * Substitute parameters in template
     * @private
     */
    _substituteParameters(template, parameters) {
        let query = template;

        // Replace ${parameter} patterns
        for (const [key, value] of Object.entries(parameters)) {
            const pattern = new RegExp(`\\$\\{${key}\\}`, 'g');
            query = query.replace(pattern, value);
        }

        // Handle optional parameters (set empty if not provided)
        const optionalParams = [
            'additionalTriples', 'additionalFilters', 'orderClause', 
            'limitClause', 'whereClause', 'insertClause'
        ];

        for (const param of optionalParams) {
            const pattern = new RegExp(`\\$\\{${param}\\}`, 'g');
            if (!parameters[param]) {
                query = query.replace(pattern, '');
            }
        }

        return query;
    }

    /**
     * Validate template parameters
     * @private
     */
    _validateParameters(template, parameters) {
        const requiredParams = template.parameters || [];
        const missing = requiredParams.filter(param => !(param in parameters));
        
        if (missing.length > 0) {
            throw new Error(`Missing required parameters: ${missing.join(', ')}`);
        }
    }

    /**
     * Basic query optimization
     * @private
     */
    _optimizeQuery(query) {
        // Remove extra whitespace and empty lines
        return query
            .replace(/\n\s*\n/g, '\n')
            .replace(/^\s+/gm, '')
            .trim();
    }
}