/**
 * ValueContext - Manages value collection and caching across rule evaluations
 * 
 * This class provides a centralized value cache and collection mechanism to:
 * 1. Eliminate redundant value fetching from the same entities/relations
 * 2. Provide efficient value aggregation and propagation
 * 3. Enable smart inference decisions based on available values
 * 4. Track value provenance and metadata for debugging
 * 
 * Usage:
 * const valueContext = new ValueContext(arbiter);
 * const result = ruleEvaluator.evaluateRule(..., { valueContext });
 */
export class ValueContext {
  constructor(arbiter, options = null) {
    this.arbiter = arbiter;
    this.partialGraphContext = options?.partialGraphContext || null;
    
    // Value cache: key -> Set of values with metadata
    this.valueCache = new Map();
    
    // Aggregated values: key -> aggregated value with metadata
    this.aggregatedCache = new Map();
    
    // Collected values from all rule evaluations
    this.allCollectedValues = [];
    
    // Track which entities/relations have been fetched
    this.fetchedRelations = new Set();
    
    // Performance tracking
    this.cacheHits = 0;
    this.cacheMisses = 0;
    this.fetchCount = 0;
  }

  /**
   * Get values from a specific entity and relation, with caching
   */
  getValues(entityId, relationName, options = {}) {
    // Use plain string key to avoid interning numbers into the UnifiedKeyManager string table
    const cacheKey = `${entityId}|${relationName}`;
    
    // Check cache first
    if (this.valueCache.has(cacheKey)) {
      this.cacheHits++;
      return this.valueCache.get(cacheKey);
    }
    
    this.cacheMisses++;
    this.fetchCount++;
    
    // Fetch from relation manager
    const valueRels = this.arbiter.relationManager.getAllValueRelationsFromSrc(entityId, relationName, {
      partialGraphContext: this.partialGraphContext
    });
    
    const values = valueRels
      .filter(rel => rel.value !== undefined && rel.value !== null)
      .map(rel => ({
        value: rel.value,
        timestamp: rel.changed_last_at || rel.updated_last_at || Date.now(),
        possibility: rel.possibility ?? 1.0,
        reliability: rel.reliability || 1.0,
        entityId: entityId,
        relationName: relationName,
        targetId: rel.dst,
        targetKey: this.arbiter.resolveKey(rel.dst, { partialGraphContext: this.partialGraphContext }),
        source: rel.source || 'persistent'
      }));
    
    // Cache the result
    this.valueCache.set(cacheKey, values);
    this.fetchedRelations.add(cacheKey);
    
    return values;
  }

  /**
   * Add collected values from a rule evaluation
   */
  addCollectedValues(collectedValues, ruleType, ruleConfig = {}) {
    if (!Array.isArray(collectedValues)) return;
    
    for (const cv of collectedValues) {
      if (!cv) continue;
      // Enhance with rule context
      const enhanced = {
        ...cv,
        ruleType,
        ruleConfig,
        collectedAt: Date.now()
      };
      
      this.allCollectedValues.push(enhanced);
      
      // Also add to entity-specific cache if not already there
      if (cv.source && cv.source.entityKey && cv.source.relation) {
        const entityId = this.arbiter.resolveNodeId(cv.source.entityKey, { partialGraphContext: this.partialGraphContext });
        if (entityId) {
          const cacheKey = `${entityId}|${cv.source.relation}`;
          if (!this.valueCache.has(cacheKey)) {
            this.valueCache.set(cacheKey, [{
              value: cv.value,
              timestamp: cv.metadata?.timestamp || Date.now(),
              possibility: cv.possibility ?? 1.0,
              reliability: cv.metadata?.reliability || 1.0,
              entityId: entityId,
              relationName: cv.source.relation,
              fromCollectedValues: true
            }]);
          }
        }
      }
    }
  }

  /**
   * Get aggregated value for a specific entity and relation
   */
  getAggregatedValue(entityId, relationName, aggregationMethod = 'max', options = {}) {
    // Use plain string key to avoid interning numbers into the UnifiedKeyManager string table
    const cacheKey = `${entityId}|${relationName}|${aggregationMethod}`;
    
    // Check aggregated cache
    if (this.aggregatedCache.has(cacheKey)) {
      return this.aggregatedCache.get(cacheKey);
    }
    
    // Get raw values
    const values = this.getValues(entityId, relationName, options);
    
    if (values.length === 0) {
      const result = {
        value: null,
        hasValue: false,
        possibility: 0,
        reliability: 0,
        timestamp: null,
        aggregationMethod,
        sourceCount: 0
      };
      this.aggregatedCache.set(cacheKey, result);
      return result;
    }
    
    // Single value case
    if (values.length === 1) {
      const result = {
        value: values[0].value,
        hasValue: true,
        possibility: values[0].possibility,
        reliability: values[0].reliability,
        timestamp: values[0].timestamp,
        aggregationMethod,
        sourceCount: 1
      };
      this.aggregatedCache.set(cacheKey, result);
      return result;
    }
    
    // Aggregate multiple values
    let aggregatedValue, aggregatedPossibility, aggregatedReliability, aggregatedTimestamp;
    
    switch (aggregationMethod) {
      case 'max':
        aggregatedValue = Math.max(...values.map(v => v.value));
        const maxEntry = values.find(v => v.value === aggregatedValue);
        aggregatedPossibility = maxEntry.possibility;
        aggregatedReliability = maxEntry.reliability;
        aggregatedTimestamp = maxEntry.timestamp;
        break;
        
      case 'min':
        aggregatedValue = Math.min(...values.map(v => v.value));
        const minEntry = values.find(v => v.value === aggregatedValue);
        aggregatedPossibility = minEntry.possibility;
        aggregatedReliability = minEntry.reliability;
        aggregatedTimestamp = minEntry.timestamp;
        break;
        
      case 'sum':
        aggregatedValue = values.reduce((sum, v) => sum + v.value, 0);
        aggregatedPossibility = Math.max(...values.map(v => v.possibility));
        aggregatedReliability = values.reduce((prod, v) => prod * v.reliability, 1.0);
        aggregatedTimestamp = Math.max(...values.map(v => v.timestamp || 0));
        break;
        
      case 'average':
        aggregatedValue = values.reduce((sum, v) => sum + v.value, 0) / values.length;
        aggregatedPossibility = values.reduce((sum, v) => sum + v.possibility, 0) / values.length;
        aggregatedReliability = values.reduce((sum, v) => sum + v.reliability, 0) / values.length;
        aggregatedTimestamp = Math.max(...values.map(v => v.timestamp || 0));
        break;
        
      default:
        // Default to max
        aggregatedValue = Math.max(...values.map(v => v.value));
        const defaultMaxEntry = values.find(v => v.value === aggregatedValue);
        aggregatedPossibility = defaultMaxEntry.possibility;
        aggregatedReliability = defaultMaxEntry.reliability;
        aggregatedTimestamp = defaultMaxEntry.timestamp;
    }
    
    const result = {
      value: aggregatedValue,
      hasValue: true,
      possibility: aggregatedPossibility,
      reliability: aggregatedReliability,
      timestamp: aggregatedTimestamp,
      aggregationMethod,
      sourceCount: values.length,
      sourceValues: values
    };
    
    this.aggregatedCache.set(cacheKey, result);
    return result;
  }

  /**
   * Check if we have sufficient values to make decisions without inference
   */
  hasSufficientValues(userKey, objectKey, relationName, options = {}) {
    const { minValueCount = 1, minReliability = 0.5 } = options;
    
    const userId = this.arbiter.nodeIdByKey.get(userKey);
    const objectId = this.arbiter.nodeIdByKey.get(objectKey);
    
    if (!userId || !objectId) return false;
    
    // Check both user and object for values
    const userValues = this.getValues(userId, relationName);
    const objectValues = this.getValues(objectId, relationName);
    
    const allValues = [...userValues, ...objectValues];
    
    if (allValues.length < minValueCount) return false;
    
    // Check reliability threshold
    const avgReliability = allValues.reduce((sum, v) => sum + v.reliability, 0) / allValues.length;
    
    return avgReliability >= minReliability;
  }

  /**
   * Get all collected values filtered by criteria
   */
  getCollectedValues(filter = {}) {
    const { ruleType, entityKey, relationName, minValue, maxValue } = filter;
    
    return this.allCollectedValues.filter(cv => {
      if (ruleType && cv.ruleType !== ruleType) return false;
      if (entityKey && cv.source?.entityKey !== entityKey) return false;
      if (relationName && cv.source?.relation !== relationName) return false;
      if (minValue !== undefined && cv.value < minValue) return false;
      if (maxValue !== undefined && cv.value > maxValue) return false;
      return true;
    });
  }

  /**
   * Merge collected values from child rule results
   */
  mergeCollectedValues(childResults) {
    const mergedValues = [];
    
    for (const result of childResults) {
      if (result.collectedValues && Array.isArray(result.collectedValues)) {
        mergedValues.push(...result.collectedValues);
        this.addCollectedValues(result.collectedValues, 'merged', { fromChildRule: true });
      }
    }
    
    return mergedValues;
  }

  /**
   * Get performance statistics
   */
  getStats() {
    return {
      cacheHits: this.cacheHits,
      cacheMisses: this.cacheMisses,
      fetchCount: this.fetchCount,
      hitRate: this.cacheHits / (this.cacheHits + this.cacheMisses),
      uniqueValuesCollected: this.allCollectedValues.length,
      uniqueRelationsFetched: this.fetchedRelations.size,
      cachedEntities: this.valueCache.size,
      aggregatedCacheSize: this.aggregatedCache.size
    };
  }

  /**
   * Clear all caches (useful for testing or memory management)
   */
  clear() {
    this.valueCache.clear();
    this.aggregatedCache.clear();
    this.allCollectedValues = [];
    this.fetchedRelations.clear();
    this.cacheHits = 0;
    this.cacheMisses = 0;
    this.fetchCount = 0;
  }

  /**
   * Invalidate caches when relations change
   * @param {string} relation - The relation that changed
   */
  invalidateCaches(relation) {
    // Clear all caches when relations change since we can't easily determine
    // which cached values are affected by the relation change
    this.valueCache.clear();
    this.aggregatedCache.clear();
    this.fetchedRelations.clear();
  }
} 
