Source: zpt/ontology/ZPTQueries.js

/**
 * ZPTQueries.js - SPARQL Query Builders for ZPT Ontology
 * 
 * This module provides SPARQL query builders and templates for working with
 * ZPT (Zoom-Pan-Tilt) navigation data integrated with Ragno corpus data.
 * 
 * Query Categories:
 * - Navigation view creation and retrieval
 * - Corpuscle selection with ZPT parameters  
 * - Navigation history and provenance tracking
 * - Optimization and analytics queries
 * - Cross-zoom and cross-session analysis
 */

import { getSPARQLPrefixes, ZPT_TERMS, RAGNO_TERMS, PROV_TERMS, XSD_TYPES } from './ZPTNamespaces.js';
import logger from 'loglevel';

/**
 * SPARQL query builder for ZPT navigation operations
 */
export class ZPTQueryBuilder {
    constructor(options = {}) {
        this.defaultGraph = options.defaultGraph || 'http://purl.org/stuff/navigation';
        this.ragnoGraph = options.ragnoGraph || 'http://purl.org/stuff/ragno';
        this.beerQAGraph = options.beerQAGraph || 'http://purl.org/stuff/beerqa';
        this.includePrefixes = options.includePrefixes !== false;
    }

    /**
     * Generate standard SPARQL prefixes for ZPT queries
     * @returns {string} SPARQL prefix declarations
     */
    getPrefixes() {
        if (!this.includePrefixes) return '';
        return getSPARQLPrefixes(['zpt', 'ragno', 'rdf', 'rdfs', 'xsd', 'prov', 'skos']);
    }

    /**
     * Create navigation view with complete state configuration
     * @param {Object} config - Navigation configuration
     * @param {string} config.viewURI - URI for the navigation view
     * @param {string} config.sessionURI - URI of parent session
     * @param {string} config.query - Natural language query
     * @param {string} config.zoomLevel - ZPT zoom level URI
     * @param {Array} config.panDomains - Array of pan domain URIs
     * @param {string} config.tiltProjection - ZPT tilt projection URI
     * @param {Array} config.selectedCorpuscles - Array of selected corpuscle URIs
     * @param {Object} config.temporalConstraint - Temporal filtering
     * @returns {string} SPARQL INSERT query
     */
    createNavigationView(config) {
        const prefixes = this.getPrefixes();
        const timestamp = new Date().toISOString();
        
        let insertData = `
INSERT DATA {
    GRAPH <${this.defaultGraph}> {
        <${config.viewURI}> a zpt:NavigationView ;
            zpt:answersQuery "${config.query}" ;
            zpt:navigationTimestamp "${timestamp}"^^xsd:dateTime `;

        // Link to session if provided
        if (config.sessionURI) {
            insertData += `;\n            zpt:partOfSession <${config.sessionURI}> `;
        }

        // Create zoom state
        const zoomStateURI = config.viewURI + '_zoom';
        insertData += `;\n            zpt:hasZoomState <${zoomStateURI}> `;
        insertData += `.\n        <${zoomStateURI}> a zpt:ZoomState ;
            zpt:atZoomLevel <${config.zoomLevel}> `;

        // Create pan state
        const panStateURI = config.viewURI + '_pan';
        insertData += `.\n        <${config.viewURI}> zpt:hasPanState <${panStateURI}> `;
        insertData += `.\n        <${panStateURI}> a zpt:PanState `;
        
        if (config.panDomains && config.panDomains.length > 0) {
            config.panDomains.forEach(domain => {
                insertData += `;\n            zpt:withPanDomain <${domain}> `;
            });
        }

        // Add temporal constraints if provided
        if (config.temporalConstraint) {
            const tempURI = config.viewURI + '_temporal';
            insertData += `;\n            zpt:hasTemporalConstraint <${tempURI}> `;
            insertData += `.\n        <${tempURI}> a zpt:TemporalConstraint `;
            
            if (config.temporalConstraint.start) {
                insertData += `;\n            zpt:temporalStart "${config.temporalConstraint.start}"^^xsd:dateTime `;
            }
            if (config.temporalConstraint.end) {
                insertData += `;\n            zpt:temporalEnd "${config.temporalConstraint.end}"^^xsd:dateTime `;
            }
        }

        // Create tilt state
        const tiltStateURI = config.viewURI + '_tilt';
        insertData += `.\n        <${config.viewURI}> zpt:hasTiltState <${tiltStateURI}> `;
        insertData += `.\n        <${tiltStateURI}> a zpt:TiltState ;
            zpt:withTiltProjection <${config.tiltProjection}> `;

        // Link to selected corpuscles
        if (config.selectedCorpuscles && config.selectedCorpuscles.length > 0) {
            config.selectedCorpuscles.forEach(corpuscle => {
                insertData += `.\n        <${config.viewURI}> zpt:selectedCorpuscle <${corpuscle}> `;
            });
        }

        insertData += `.\n    }\n}`;

        return prefixes + insertData;
    }

    /**
     * Query for corpuscles based on ZPT navigation parameters
     * @param {Object} params - Query parameters
     * @param {string} params.zoomLevel - ZPT zoom level URI
     * @param {Array} params.panDomains - Pan domain filters
     * @param {string} params.tiltProjection - Tilt projection method
     * @param {Object} params.temporalConstraint - Temporal filtering
     * @param {number} params.limit - Result limit
     * @returns {string} SPARQL SELECT query
     */
    selectCorpusclesByNavigation(params) {
        const prefixes = this.getPrefixes();
        const limit = params.limit || 100;
        
        let query = `
SELECT DISTINCT ?corpuscle ?content ?embedding 
    (COALESCE(?optScore, 0.0) AS ?optimizationScore)
    (COALESCE(?zoomRel, 0.0) AS ?zoomRelevance)
    (COALESCE(?panCov, 0.0) AS ?panCoverage)
    (COALESCE(?tiltEff, 0.0) AS ?tiltEffectiveness)
WHERE {`;

        // Query corpuscles from appropriate graph based on zoom level
        const corpusGraph = this.getCorpusGraphForZoom(params.zoomLevel);
        query += `
    GRAPH <${corpusGraph}> {
        ?corpuscle a ragno:Corpuscle ;
                   ragno:content ?content .
        OPTIONAL { ?corpuscle ragno:hasEmbedding ?embedding }
    }`;

        // Add optimization metadata if available
        query += `
    OPTIONAL {
        GRAPH <${this.defaultGraph}> {
            ?corpuscle zpt:optimizationScore ?optScore ;
                      zpt:zoomRelevance ?zoomRel ;
                      zpt:panCoverage ?panCov ;
                      zpt:tiltEffectiveness ?tiltEff .
        }
    }`;

        // Apply zoom level filtering
        if (params.zoomLevel) {
            query += this.getZoomLevelFilter(params.zoomLevel);
        }

        // Apply pan domain filtering
        if (params.panDomains && params.panDomains.length > 0) {
            query += this.getPanDomainFilter(params.panDomains);
        }

        // Apply temporal filtering
        if (params.temporalConstraint) {
            query += this.getTemporalFilter(params.temporalConstraint);
        }

        // Apply tilt projection filtering
        if (params.tiltProjection) {
            query += this.getTiltProjectionFilter(params.tiltProjection);
        }

        query += `
}
ORDER BY DESC(?optimizationScore) DESC(?zoomRelevance)
LIMIT ${limit}`;

        return prefixes + query;
    }

    /**
     * Query navigation views by parameters
     * @param {Object} filters - Filter parameters
     * @param {string} filters.zoomLevel - Filter by zoom level
     * @param {string} filters.sessionURI - Filter by session
     * @param {Date} filters.startTime - Filter by start time
     * @param {Date} filters.endTime - Filter by end time
     * @returns {string} SPARQL SELECT query
     */
    queryNavigationViews(filters = {}) {
        const prefixes = this.getPrefixes();
        
        let query = `
SELECT ?view ?query ?timestamp ?session ?zoomLevel ?tiltProjection
    (GROUP_CONCAT(DISTINCT ?panDomain; separator=",") AS ?panDomains)
    (COUNT(DISTINCT ?selectedCorpuscle) AS ?corpuscleCount)
WHERE {
    GRAPH <${this.defaultGraph}> {
        ?view a zpt:NavigationView ;
              zpt:answersQuery ?query ;
              zpt:navigationTimestamp ?timestamp .
              
        OPTIONAL { ?view zpt:partOfSession ?session }
        OPTIONAL { ?view zpt:selectedCorpuscle ?selectedCorpuscle }
        
        ?view zpt:hasZoomState [ zpt:atZoomLevel ?zoomLevel ] ;
              zpt:hasTiltState [ zpt:withTiltProjection ?tiltProjection ] .
              
        OPTIONAL {
            ?view zpt:hasPanState [ zpt:withPanDomain ?panDomain ]
        }`;

        // Apply filters
        if (filters.zoomLevel) {
            query += `
        FILTER(?zoomLevel = <${filters.zoomLevel}>)`;
        }

        if (filters.sessionURI) {
            query += `
        FILTER(?session = <${filters.sessionURI}>)`;
        }

        if (filters.startTime) {
            query += `
        FILTER(?timestamp >= "${filters.startTime.toISOString()}"^^xsd:dateTime)`;
        }

        if (filters.endTime) {
            query += `
        FILTER(?timestamp <= "${filters.endTime.toISOString()}"^^xsd:dateTime)`;
        }

        query += `
    }
}
GROUP BY ?view ?query ?timestamp ?session ?zoomLevel ?tiltProjection
ORDER BY DESC(?timestamp)`;

        return prefixes + query;
    }

    /**
     * Query navigation history and provenance
     * @param {Object} params - Query parameters
     * @param {string} params.sessionURI - Session to analyze
     * @param {string} params.agentURI - Agent to track
     * @returns {string} SPARQL SELECT query
     */
    queryNavigationHistory(params = {}) {
        const prefixes = this.getPrefixes();
        
        let query = `
SELECT ?view ?previousView ?query ?timestamp ?zoomLevel ?duration
WHERE {
    GRAPH <${this.defaultGraph}> {
        ?view a zpt:NavigationView ;
              zpt:answersQuery ?query ;
              zpt:navigationTimestamp ?timestamp ;
              zpt:hasZoomState [ zpt:atZoomLevel ?zoomLevel ] .
              
        OPTIONAL { ?view zpt:previousView ?previousView }`;

        if (params.sessionURI) {
            query += `
        ?view zpt:partOfSession <${params.sessionURI}> .`;
        }

        if (params.agentURI) {
            query += `
        ?view zpt:navigatedBy <${params.agentURI}> .`;
        }

        query += `
        
        # Calculate duration between views
        OPTIONAL {
            ?nextView zpt:previousView ?view ;
                     zpt:navigationTimestamp ?nextTimestamp .
            BIND(?nextTimestamp - ?timestamp AS ?duration)
        }
    }
}
ORDER BY ?timestamp`;

        return prefixes + query;
    }

    /**
     * Query for cross-zoom navigation patterns
     * @param {string} corpuscleURI - Corpuscle to analyze
     * @returns {string} SPARQL SELECT query
     */
    queryCrossZoomNavigation(corpuscleURI) {
        const prefixes = this.getPrefixes();
        
        return prefixes + `
SELECT ?view1 ?view2 ?zoom1 ?zoom2 ?query1 ?query2 ?timestamp1 ?timestamp2
WHERE {
    GRAPH <${this.defaultGraph}> {
        ?view1 a zpt:NavigationView ;
               zpt:selectedCorpuscle <${corpuscleURI}> ;
               zpt:answersQuery ?query1 ;
               zpt:navigationTimestamp ?timestamp1 ;
               zpt:hasZoomState [ zpt:atZoomLevel ?zoom1 ] .
               
        ?view2 a zpt:NavigationView ;
               zpt:selectedCorpuscle <${corpuscleURI}> ;
               zpt:answersQuery ?query2 ;
               zpt:navigationTimestamp ?timestamp2 ;
               zpt:hasZoomState [ zpt:atZoomLevel ?zoom2 ] .
               
        FILTER(?view1 != ?view2)
        FILTER(?zoom1 != ?zoom2)
    }
}
ORDER BY ?timestamp1 ?timestamp2`;
    }

    /**
     * Update optimization scores for corpuscles
     * @param {Array} scores - Array of score objects
     * @returns {string} SPARQL UPDATE query
     */
    updateOptimizationScores(scores) {
        const prefixes = this.getPrefixes();
        
        let deleteClause = 'DELETE {\n    GRAPH <' + this.defaultGraph + '> {\n';
        let insertClause = 'INSERT {\n    GRAPH <' + this.defaultGraph + '> {\n';
        let whereClause = 'WHERE {\n';

        scores.forEach((score, index) => {
            const corpuscleVar = `?corpuscle${index}`;
            
            // Delete existing scores
            deleteClause += `        ${corpuscleVar} zpt:optimizationScore ?oldOpt${index} ;
                      zpt:zoomRelevance ?oldZoom${index} ;
                      zpt:panCoverage ?oldPan${index} ;
                      zpt:tiltEffectiveness ?oldTilt${index} .\n`;
            
            // Insert new scores
            insertClause += `        <${score.corpuscleURI}> zpt:optimizationScore ${score.optimizationScore} ;
                      zpt:zoomRelevance ${score.zoomRelevance} ;
                      zpt:panCoverage ${score.panCoverage} ;
                      zpt:tiltEffectiveness ${score.tiltEffectiveness} .\n`;
            
            // Where conditions
            whereClause += `    OPTIONAL {
        GRAPH <${this.defaultGraph}> {
            <${score.corpuscleURI}> zpt:optimizationScore ?oldOpt${index} ;
                      zpt:zoomRelevance ?oldZoom${index} ;
                      zpt:panCoverage ?oldPan${index} ;
                      zpt:tiltEffectiveness ?oldTilt${index} .
        }
    }\n`;
        });

        deleteClause += '    }\n}\n';
        insertClause += '    }\n}\n';
        whereClause += '}';

        return prefixes + deleteClause + insertClause + whereClause;
    }

    /**
     * Query for navigation analytics and patterns
     * @param {Object} params - Analytics parameters
     * @returns {string} SPARQL SELECT query
     */
    queryNavigationAnalytics(params = {}) {
        const prefixes = this.getPrefixes();
        
        return prefixes + `
SELECT ?zoomLevel ?tiltProjection 
    (COUNT(?view) AS ?viewCount)
    (AVG(?corpuscleCount) AS ?avgCorpuscleCount)
    (MIN(?timestamp) AS ?firstUse)
    (MAX(?timestamp) AS ?lastUse)
WHERE {
    GRAPH <${this.defaultGraph}> {
        ?view a zpt:NavigationView ;
              zpt:navigationTimestamp ?timestamp ;
              zpt:hasZoomState [ zpt:atZoomLevel ?zoomLevel ] ;
              zpt:hasTiltState [ zpt:withTiltProjection ?tiltProjection ] .
              
        {
            SELECT ?view (COUNT(?corpuscle) AS ?corpuscleCount) WHERE {
                ?view zpt:selectedCorpuscle ?corpuscle .
            }
            GROUP BY ?view
        }
    }
}
GROUP BY ?zoomLevel ?tiltProjection
ORDER BY DESC(?viewCount)`;
    }

    /**
     * Helper: Get appropriate corpus graph for zoom level
     * @param {string} zoomLevelURI - ZPT zoom level URI
     * @returns {string} Graph URI for corpus data
     */
    getCorpusGraphForZoom(zoomLevelURI) {
        // Map zoom levels to appropriate data graphs
        if (zoomLevelURI.includes('EntityLevel') || zoomLevelURI.includes('UnitLevel')) {
            return this.beerQAGraph; // Prefer BeerQA for entity/unit level
        }
        return this.ragnoGraph; // Default to Ragno graph
    }

    /**
     * Helper: Generate zoom level filter
     * @param {string} zoomLevelURI - ZPT zoom level URI
     * @returns {string} SPARQL filter clause
     */
    getZoomLevelFilter(zoomLevelURI) {
        // This could be enhanced to filter based on zoom level semantics
        // For now, we rely on the corpus graph selection
        return '';
    }

    /**
     * Helper: Generate pan domain filter
     * @param {Array} panDomains - Array of pan domain URIs
     * @returns {string} SPARQL filter clause
     */
    getPanDomainFilter(panDomains) {
        if (!panDomains || panDomains.length === 0) return '';
        
        // Create content-based filtering for pan domains
        const domainFilters = panDomains.map(domain => {
            // Extract domain name for content filtering
            const domainName = domain.split('/').pop().toLowerCase();
            return `CONTAINS(LCASE(?content), "${domainName}")`;
        }).join(' || ');
        
        return `
    FILTER(${domainFilters})`;
    }

    /**
     * Helper: Generate temporal filter
     * @param {Object} temporalConstraint - Temporal constraints
     * @returns {string} SPARQL filter clause
     */
    getTemporalFilter(temporalConstraint) {
        let filter = '';
        
        if (temporalConstraint.start || temporalConstraint.end) {
            // This assumes corpuscles have temporal metadata
            filter += `
    OPTIONAL { ?corpuscle ragno:timestamp ?corpTimestamp }`;
            
            if (temporalConstraint.start) {
                filter += `
    FILTER(!BOUND(?corpTimestamp) || ?corpTimestamp >= "${temporalConstraint.start}"^^xsd:dateTime)`;
            }
            
            if (temporalConstraint.end) {
                filter += `
    FILTER(!BOUND(?corpTimestamp) || ?corpTimestamp <= "${temporalConstraint.end}"^^xsd:dateTime)`;
            }
        }
        
        return filter;
    }

    /**
     * Helper: Generate tilt projection filter
     * @param {string} tiltProjectionURI - ZPT tilt projection URI
     * @returns {string} SPARQL filter clause
     */
    getTiltProjectionFilter(tiltProjectionURI) {
        // Filter based on tilt projection requirements
        if (tiltProjectionURI.includes('EmbeddingProjection')) {
            return `
    FILTER(BOUND(?embedding))`;
        }
        
        // Keywords, Graph, and Temporal projections don't require special filtering here
        return '';
    }
}

/**
 * Pre-built query templates for common operations
 */
export const ZPTQueryTemplates = {
    /**
     * Find all navigation views for a specific query
     */
    VIEWS_BY_QUERY: `
SELECT ?view ?timestamp ?zoomLevel ?tiltProjection ?corpuscleCount WHERE {
    GRAPH <NAVIGATION_GRAPH> {
        ?view a zpt:NavigationView ;
              zpt:answersQuery "QUERY_TEXT" ;
              zpt:navigationTimestamp ?timestamp ;
              zpt:hasZoomState [ zpt:atZoomLevel ?zoomLevel ] ;
              zpt:hasTiltState [ zpt:withTiltProjection ?tiltProjection ] .
        
        {
            SELECT ?view (COUNT(?corpuscle) AS ?corpuscleCount) WHERE {
                ?view zpt:selectedCorpuscle ?corpuscle .
            }
            GROUP BY ?view
        }
    }
}
ORDER BY DESC(?timestamp)`,

    /**
     * Find corpuscles selected across multiple zoom levels
     */
    CROSS_ZOOM_CORPUSCLES: `
SELECT ?corpuscle (COUNT(DISTINCT ?zoomLevel) AS ?zoomCount) 
       (GROUP_CONCAT(DISTINCT ?zoomLevel; separator=",") AS ?zoomLevels) WHERE {
    GRAPH <NAVIGATION_GRAPH> {
        ?view zpt:selectedCorpuscle ?corpuscle ;
              zpt:hasZoomState [ zpt:atZoomLevel ?zoomLevel ] .
    }
}
GROUP BY ?corpuscle
HAVING(?zoomCount > 1)
ORDER BY DESC(?zoomCount)`,

    /**
     * Navigation session summary
     */
    SESSION_SUMMARY: `
SELECT ?session ?duration ?viewCount ?uniqueCorpuscles 
       (MIN(?timestamp) AS ?startTime)
       (MAX(?timestamp) AS ?endTime) WHERE {
    GRAPH <NAVIGATION_GRAPH> {
        ?session a zpt:NavigationSession .
        OPTIONAL { ?session zpt:sessionDuration ?duration }
        
        ?view zpt:partOfSession ?session ;
              zpt:navigationTimestamp ?timestamp .
              
        OPTIONAL { ?view zpt:selectedCorpuscle ?corpuscle }
    }
}
GROUP BY ?session ?duration`,

    /**
     * Most effective tilt projections by optimization score
     */
    TILT_EFFECTIVENESS: `
SELECT ?tiltProjection 
       (AVG(?optimizationScore) AS ?avgScore)
       (COUNT(?view) AS ?usage) WHERE {
    GRAPH <NAVIGATION_GRAPH> {
        ?view zpt:hasTiltState [ zpt:withTiltProjection ?tiltProjection ] ;
              zpt:selectedCorpuscle ?corpuscle .
        ?corpuscle zpt:optimizationScore ?optimizationScore .
    }
}
GROUP BY ?tiltProjection
ORDER BY DESC(?avgScore)`
};

/**
 * Utility functions for query building
 */
export const ZPTQueryUtils = {
    /**
     * Replace template placeholders with actual values
     * @param {string} template - Query template
     * @param {Object} params - Replacement parameters
     * @returns {string} Complete SPARQL query
     */
    buildFromTemplate(template, params = {}) {
        let query = getSPARQLPrefixes() + '\n' + template;
        
        // Replace common placeholders
        Object.entries(params).forEach(([key, value]) => {
            const placeholder = key.toUpperCase().replace(/([A-Z])/g, '_$1').substring(1);
            query = query.replace(new RegExp(placeholder, 'g'), value);
        });
        
        return query;
    },

    /**
     * Build INSERT query for bulk corpuscle optimization scores
     * @param {Array} corpuscleScores - Array of corpuscle score objects
     * @param {string} graph - Target graph URI
     * @returns {string} SPARQL INSERT query
     */
    buildBulkScoreInsert(corpuscleScores, graph) {
        const prefixes = getSPARQLPrefixes();
        
        let insertData = `INSERT DATA {\n    GRAPH <${graph}> {\n`;
        
        corpuscleScores.forEach(score => {
            insertData += `        <${score.corpuscleURI}> zpt:optimizationScore ${score.optimizationScore} ;\n`;
            insertData += `                                zpt:zoomRelevance ${score.zoomRelevance} ;\n`;
            insertData += `                                zpt:panCoverage ${score.panCoverage} ;\n`;
            insertData += `                                zpt:tiltEffectiveness ${score.tiltEffectiveness} .\n`;
        });
        
        insertData += `    }\n}`;
        
        return prefixes + insertData;
    },

    /**
     * Validate SPARQL query syntax (basic check)
     * @param {string} query - SPARQL query to validate
     * @returns {Object} Validation result
     */
    validateQuery(query) {
        const errors = [];
        const warnings = [];
        
        // Basic syntax checks
        if (!query.trim()) {
            errors.push('Query is empty');
        }
        
        // Check for required keywords
        const hasSelect = /SELECT/i.test(query);
        const hasInsert = /INSERT/i.test(query);
        const hasConstruct = /CONSTRUCT/i.test(query);
        const hasAsk = /ASK/i.test(query);
        
        if (!(hasSelect || hasInsert || hasConstruct || hasAsk)) {
            errors.push('Query must contain SELECT, INSERT, CONSTRUCT, or ASK');
        }
        
        // Check for unbalanced braces
        const openBraces = (query.match(/{/g) || []).length;
        const closeBraces = (query.match(/}/g) || []).length;
        if (openBraces !== closeBraces) {
            errors.push('Unbalanced braces in query');
        }
        
        // Check for ZPT namespace usage
        if (/zpt:/.test(query) && !/PREFIX zpt:/i.test(query)) {
            warnings.push('ZPT namespace used but not declared');
        }
        
        return {
            valid: errors.length === 0,
            errors,
            warnings
        };
    }
};

export default ZPTQueryBuilder;