Source: aux/wikidata/WikidataNavigator.js

/**
 * WikidataNavigator - Enhanced ZPT navigation with Wikidata integration
 * 
 * This component provides enhanced semantic navigation by integrating Wikidata
 * entities into ZPT navigation workflows. It creates cross-references between
 * local knowledge and global Wikidata entities for richer navigation contexts.
 * 
 * API: enhanceNavigation(input, resources, options)
 */

import logger from 'loglevel';
import { ZPTDataFactory } from '../../zpt/ontology/ZPTDataFactory.js';
import { NamespaceUtils, getSPARQLPrefixes } from '../../zpt/ontology/ZPTNamespaces.js';
import WikidataResearcher from './WikidataResearcher.js';

export default class WikidataNavigator {
    constructor() {
        this.wikidataResearcher = new WikidataResearcher();
        this.navigationSessions = [];
    }

    /**
     * Enhance ZPT navigation with Wikidata integration
     * 
     * @param {Object} input - Navigation input data
     * @param {Object} input.question - Question object with text and uri
     * @param {Array<Object>} input.localEntities - Local entities from BeerQA navigation
     * @param {Object} input.navigationContext - Existing navigation context
     * @param {Object} resources - External dependencies
     * @param {Object} resources.llmHandler - LLM handler for concept extraction
     * @param {Object} resources.sparqlHelper - SPARQL helper for database operations
     * @param {Object} resources.config - Configuration object with graph URIs
     * @param {Object} resources.embeddingHandler - Embedding handler for similarity (optional)
     * @param {Object} options - Configuration options
     * @param {boolean} options.enableWikidataResearch - Enable Wikidata research (default: true)
     * @param {number} options.maxWikidataEntities - Max Wikidata entities (default: 10)
     * @param {boolean} options.createCrossReferences - Create cross-references (default: true)
     * @param {number} options.similarityThreshold - Similarity threshold for matching (default: 0.7)
     * @returns {Promise<Object>} Enhanced navigation results
     */
    async enhanceNavigation(input, resources, options = {}) {
        const startTime = Date.now();
        
        try {
            const { question, localEntities = [], navigationContext = {} } = input;
            const { llmHandler, sparqlHelper, config, embeddingHandler } = resources;
            
            const navConfig = {
                enableWikidataResearch: options.enableWikidataResearch !== false,
                maxWikidataEntities: options.maxWikidataEntities || 10,
                createCrossReferences: options.createCrossReferences !== false,
                similarityThreshold: options.similarityThreshold || 0.7,
                enhancementLevel: options.enhancementLevel || 'standard',
                ...options
            };

            const enhancementResult = {
                originalEntities: localEntities,
                wikidataEntities: [],
                crossReferences: [],
                enhancedContext: navigationContext,
                relationships: [],
                metadata: {
                    startTime: new Date().toISOString(),
                    enhancementLevel: navConfig.enhancementLevel
                }
            };

            // Step 1: Wikidata research (if enabled)
            if (navConfig.enableWikidataResearch) {
                const researchResult = await this._conductWikidataResearch(
                    question,
                    llmHandler,
                    sparqlHelper,
                    config,
                    navConfig
                );

                if (researchResult.success) {
                    enhancementResult.wikidataEntities = researchResult.ragnoEntities.slice(0, navConfig.maxWikidataEntities);
                    enhancementResult.metadata.wikidataResearchDuration = researchResult.metadata.researchDuration;
                }
            }

            // Step 2: Create cross-references between local and Wikidata entities
            if (navConfig.createCrossReferences && enhancementResult.wikidataEntities.length > 0) {
                const crossRefResult = await this._createCrossReferences(
                    localEntities,
                    enhancementResult.wikidataEntities,
                    embeddingHandler,
                    navConfig
                );

                if (crossRefResult.success) {
                    enhancementResult.crossReferences = crossRefResult.crossReferences;
                    enhancementResult.relationships = crossRefResult.relationships;
                }
            }

            // Step 3: Build enhanced navigation context
            const contextResult = await this._buildEnhancedContext(
                enhancementResult,
                question,
                navConfig
            );

            if (contextResult.success) {
                enhancementResult.enhancedContext = contextResult.context;
                enhancementResult.metadata.contextEnhancement = contextResult.metadata;
            }

            // Step 4: Store navigation session
            const sessionData = {
                questionText: question.text,
                questionURI: question.uri,
                localEntitiesCount: localEntities.length,
                wikidataEntitiesCount: enhancementResult.wikidataEntities.length,
                crossReferencesCount: enhancementResult.crossReferences.length,
                enhancementLevel: navConfig.enhancementLevel,
                duration: Date.now() - startTime,
                timestamp: new Date().toISOString()
            };

            this.navigationSessions.push(sessionData);

            // Final metadata
            enhancementResult.metadata = {
                ...enhancementResult.metadata,
                totalDuration: Date.now() - startTime,
                endTime: new Date().toISOString(),
                entitiesTotal: localEntities.length + enhancementResult.wikidataEntities.length,
                enhancementSuccessful: true
            };

            return {
                success: true,
                data: enhancementResult,
                metadata: enhancementResult.metadata
            };

        } catch (error) {
            logger.error('Navigation enhancement failed:', error.message);
            return {
                success: false,
                error: error.message,
                data: {
                    originalEntities: input.localEntities || [],
                    wikidataEntities: [],
                    crossReferences: [],
                    enhancedContext: input.navigationContext || {},
                    relationships: []
                },
                metadata: {
                    totalDuration: Date.now() - startTime,
                    errorOccurred: true,
                    timestamp: new Date().toISOString()
                }
            };
        }
    }

    /**
     * Get enhanced entity context for questions
     * 
     * @param {Object} input - Context input data
     * @param {Array<string>} input.entityURIs - Entity URIs to get context for
     * @param {Object} input.question - Question object
     * @param {Object} resources - External dependencies
     * @param {Object} resources.sparqlHelper - SPARQL helper for database operations
     * @param {Object} resources.config - Configuration object with graph URIs
     * @param {Object} options - Configuration options
     * @returns {Promise<Object>} Enhanced entity context
     */
    async getEnhancedEntityContext(input, resources, options = {}) {
        try {
            const { entityURIs, question } = input;
            const { sparqlHelper, config } = resources;

            if (!entityURIs || entityURIs.length === 0) {
                return {
                    success: true,
                    entities: [],
                    metadata: { contextType: 'empty' }
                };
            }

            // Query multiple graphs for comprehensive context
            const contextQuery = `
PREFIX ragno: <http://purl.org/stuff/ragno/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT ?entity ?graph ?label ?content ?embedding ?conceptValue ?relationshipWeight
WHERE {
    VALUES ?entity { ${entityURIs.map(uri => `<${uri}>`).join(' ')} }
    
    GRAPH ?graph {
        ?entity a ragno:Corpuscle ;
               rdfs:label ?label .
        
        OPTIONAL {
            ?entity ragno:hasTextElement ?textElement .
            ?textElement ragno:content ?content .
        }
        
        OPTIONAL {
            ?entity ragno:hasAttribute ?embeddingAttr .
            ?embeddingAttr ragno:attributeType "vector-embedding" ;
                          ragno:attributeValue ?embedding .
        }
        
        OPTIONAL {
            ?entity ragno:hasAttribute ?conceptAttr .
            ?conceptAttr ragno:attributeType "concept" ;
                        ragno:attributeValue ?conceptValue .
        }
        
        OPTIONAL {
            ?relationship ragno:hasTargetEntity ?entity ;
                         ragno:hasSourceEntity <${question.uri}> ;
                         ragno:weight ?relationshipWeight .
        }
    }
    
    FILTER(?graph IN (
        <${config.beerqaGraphURI}>, 
        <${config.wikipediaGraphURI}>, 
        <${config.wikidataGraphURI}>
    ))
}
ORDER BY DESC(?relationshipWeight) ?label`;

            const result = await sparqlHelper.executeSelect(contextQuery);

            if (result.success) {
                const entities = this._processEntityContext(result.data.results.bindings);
                
                return {
                    success: true,
                    entities,
                    metadata: {
                        entitiesFound: entities.length,
                        graphsQueried: 3,
                        contextType: 'enhanced-multi-graph'
                    }
                };
            } else {
                throw new Error(result.error || 'Context query failed');
            }

        } catch (error) {
            logger.error('Failed to get enhanced entity context:', error.message);
            return {
                success: false,
                error: error.message,
                entities: [],
                metadata: {
                    errorOccurred: true,
                    timestamp: new Date().toISOString()
                }
            };
        }
    }

    /**
     * Get navigation statistics
     * 
     * @param {Object} input - Statistics input (can be empty)
     * @param {Object} resources - External dependencies (unused)
     * @param {Object} options - Configuration options
     * @returns {Object} Navigation statistics
     */
    getNavigationStatistics(input = {}, resources = {}, options = {}) {
        const stats = {
            totalNavigationSessions: this.navigationSessions.length,
            averageWikidataEntities: 0,
            averageCrossReferences: 0,
            averageDuration: 0,
            enhancementLevels: {}
        };

        if (this.navigationSessions.length > 0) {
            stats.averageWikidataEntities = Math.round(
                this.navigationSessions.reduce((sum, s) => sum + s.wikidataEntitiesCount, 0) / 
                this.navigationSessions.length
            );
            
            stats.averageCrossReferences = Math.round(
                this.navigationSessions.reduce((sum, s) => sum + s.crossReferencesCount, 0) / 
                this.navigationSessions.length
            );
            
            stats.averageDuration = Math.round(
                this.navigationSessions.reduce((sum, s) => sum + s.duration, 0) / 
                this.navigationSessions.length
            );

            // Count enhancement levels
            this.navigationSessions.forEach(session => {
                const level = session.enhancementLevel || 'unknown';
                stats.enhancementLevels[level] = (stats.enhancementLevels[level] || 0) + 1;
            });
        }

        return {
            success: true,
            statistics: stats,
            metadata: {
                timestamp: new Date().toISOString()
            }
        };
    }

    /**
     * Conduct Wikidata research for the question
     * @private
     */
    async _conductWikidataResearch(question, llmHandler, sparqlHelper, config, navConfig) {
        return await this.wikidataResearcher.executeResearch(
            { question: question.text },
            { llmHandler, sparqlHelper, config },
            {
                maxWikidataSearchResults: navConfig.maxWikidataEntities,
                storeResults: true,
                storageGraph: config.wikidataGraphURI
            }
        );
    }

    /**
     * Create cross-references between local and Wikidata entities
     * @private
     */
    async _createCrossReferences(localEntities, wikidataEntities, embeddingHandler, navConfig) {
        try {
            const crossReferences = [];
            const relationships = [];

            // Simple text-based matching for now
            // In production, this would use embeddings for semantic similarity
            for (const localEntity of localEntities) {
                for (const wikidataEntity of wikidataEntities) {
                    const similarity = this._calculateTextSimilarity(
                        localEntity.label || localEntity.content || '',
                        wikidataEntity.label || wikidataEntity.description || ''
                    );

                    if (similarity >= navConfig.similarityThreshold) {
                        const crossRef = {
                            localEntity: localEntity.uri,
                            wikidataEntity: wikidataEntity.uri,
                            similarity: similarity,
                            type: 'semantic-similarity',
                            confidence: similarity
                        };

                        crossReferences.push(crossRef);

                        // Create relationship
                        const relationship = {
                            uri: `${localEntity.uri}/related/${Date.now()}`,
                            sourceEntity: localEntity.uri,
                            targetEntity: wikidataEntity.uri,
                            relationshipType: 'ragno:relatedTo',
                            weight: similarity,
                            source: 'wikidata-enhancement'
                        };

                        relationships.push(relationship);
                    }
                }
            }

            return {
                success: true,
                crossReferences,
                relationships,
                metadata: {
                    localEntitiesProcessed: localEntities.length,
                    wikidataEntitiesProcessed: wikidataEntities.length,
                    crossReferencesCreated: crossReferences.length
                }
            };

        } catch (error) {
            logger.error('Failed to create cross-references:', error.message);
            return {
                success: false,
                error: error.message,
                crossReferences: [],
                relationships: []
            };
        }
    }

    /**
     * Build enhanced navigation context
     * @private
     */
    async _buildEnhancedContext(enhancementResult, question, navConfig) {
        try {
            const context = {
                question: question.text,
                localEntitiesCount: enhancementResult.originalEntities.length,
                wikidataEntitiesCount: enhancementResult.wikidataEntities.length,
                crossReferencesCount: enhancementResult.crossReferences.length,
                enhancementLevel: navConfig.enhancementLevel,
                contextSummary: '',
                entities: [
                    ...enhancementResult.originalEntities,
                    ...enhancementResult.wikidataEntities
                ],
                relationships: enhancementResult.relationships
            };

            // Build context summary
            const totalEntities = context.localEntitiesCount + context.wikidataEntitiesCount;
            context.contextSummary = `Enhanced navigation context with ${totalEntities} entities ` +
                `(${context.localEntitiesCount} local, ${context.wikidataEntitiesCount} Wikidata) ` +
                `and ${context.crossReferencesCount} cross-references.`;

            return {
                success: true,
                context,
                metadata: {
                    contextBuilt: true,
                    totalEntities: totalEntities,
                    timestamp: new Date().toISOString()
                }
            };

        } catch (error) {
            logger.error('Failed to build enhanced context:', error.message);
            return {
                success: false,
                error: error.message,
                context: {}
            };
        }
    }

    /**
     * Process entity context from SPARQL results
     * @private
     */
    _processEntityContext(bindings) {
        const entityMap = new Map();

        bindings.forEach(binding => {
            const uri = binding.entity.value;
            
            if (!entityMap.has(uri)) {
                entityMap.set(uri, {
                    uri: uri,
                    graph: binding.graph.value,
                    label: binding.label?.value || '',
                    content: binding.content?.value || '',
                    embedding: binding.embedding?.value || null,
                    concepts: [],
                    relationshipWeight: parseFloat(binding.relationshipWeight?.value || 0)
                });
            }

            // Add concept if present
            if (binding.conceptValue?.value) {
                const entity = entityMap.get(uri);
                if (!entity.concepts.includes(binding.conceptValue.value)) {
                    entity.concepts.push(binding.conceptValue.value);
                }
            }
        });

        return Array.from(entityMap.values())
            .sort((a, b) => b.relationshipWeight - a.relationshipWeight);
    }

    /**
     * Calculate simple text similarity
     * @private
     */
    _calculateTextSimilarity(text1, text2) {
        if (!text1 || !text2) return 0;
        
        const words1 = text1.toLowerCase().split(/\s+/);
        const words2 = text2.toLowerCase().split(/\s+/);
        
        const intersection = words1.filter(word => words2.includes(word));
        const union = [...new Set([...words1, ...words2])];
        
        return union.length > 0 ? intersection.length / union.length : 0;
    }

    /**
     * Clear navigation statistics
     */
    clearStatistics() {
        this.navigationSessions = [];
        this.wikidataResearcher.clearStatistics();
    }
}