import { generateStateId } from '../../utils/stateId.js';

function validatePossibility(possibility) {
  if (typeof possibility !== 'number' || !Number.isFinite(possibility) || possibility < 0 || possibility > 1) {
    throw new Error(`Invalid possibility: expected a finite number in [0, 1], got ${String(possibility)}`);
  }
  return possibility;
}

export class RelationUpdates {
  constructor(manager) {
    this.manager = manager;
  }

  addRelation(srcKey, relation, dstKey, options) {
    if (this.manager.arbiter._snapshotReadOnly) {
      throw new Error('Cannot add relation while in snapshot read-only mode. Disable snapshot before writing.');
    }
    const srcId = this.manager.arbiter.nodeIdByKey.get(srcKey);
    const dstId = this.manager.arbiter.nodeIdByKey.get(dstKey);
    if (srcId === undefined || dstId === undefined) return;
    
    // Only check for existing relations if NOT in fast construction mode or batch update mode
    // This avoids O(n²) performance during bulk loading
    if (!this.manager.arbiter.fastConstructionMode && !this.manager.arbiter.batchUpdateInProgress) {
      // Use O(1) Set lookup instead of O(n) Array.find
      const relationKey = this.manager._makeRelationKey(srcId, relation, dstId);
      if (this.manager._relationKeys.has(relationKey)) {
        // Update existing relation
        return this._modifyRelation(srcKey, relation, dstKey, options);
      }
    }
    
    // Add new relation
    const currentTime = Date.now();
    const newStateId = generateStateId();

    const relationObj = { 
      src: srcId, 
      rel: relation, 
      dst: dstId,
      possibility: options?.possibility !== undefined ? validatePossibility(options.possibility) : 1.0,
      reliability: options?.reliability !== undefined ? options.reliability : 1.0,
      value: options?.value,
      decayConfig: options?.decayConfig,
      updated_last_at: currentTime,
      changed_last_at: options?.changed_last_at !== undefined ? options.changed_last_at : currentTime,
      stateId: newStateId,
      validity: options?.validity !== undefined ? options.validity : null
    };
    
    this.manager.arbiter.relations.push(relationObj);
    this.manager._relationsChanged = true;

    const relationIndex = this.manager.arbiter.relations.length - 1;
    const relationKey = this.manager._makeRelationKey(srcId, relation, dstId);
    this.manager._relationKeys.add(relationKey);
    this.manager._relationKeyToIndex.set(relationKey, relationIndex);
    
    // Invalidate affected caches
    this.manager._invalidateRelationCaches(srcId, relation, dstId);
    this.manager._invalidateValueRelationCaches(srcId, dstId, relation);
    
    // Embedding/similarity updates disabled
    
    // ALWAYS maintain indices incrementally for optimal performance
    if (this.manager.arbiter._snapshotReadOnly) {
      throw new Error('Cannot add relation while in snapshot read-only mode. Disable snapshot before writing.');
    }
    this.manager.arbiter.indices.addRelation(relationObj);
    this.manager.arbiter.indicesBuilt = true;

    this.manager._addRelationToGraph(srcId, relation, dstId);
    
    // Update PLTC indices incrementally (if initialized and not in batch mode)
    if (!this.manager.arbiter.batchUpdateInProgress) {
      this.manager._updatePLTCIndicesOnAdd(srcId, dstId, relation);
    }
    
    // Embedding/similarity updates disabled
    
    return relationIndex;
  }

  removeRelation(srcKey, relationName, dstKey, attributes) {
    if (this.manager.arbiter._snapshotReadOnly) {
      throw new Error('Cannot remove relation while in snapshot read-only mode. Disable snapshot before writing.');
    }
    const srcId = this.manager.arbiter.nodeManager.getNodeId(srcKey);
    const dstId = this.manager.arbiter.nodeManager.getNodeId(dstKey);

    if (srcId === undefined || dstId === undefined) {
      return false;
    }

    const relationKey = this.manager._makeRelationKey(srcId, relationName, dstId);
    const relationIndex = this.manager._relationKeyToIndex.get(relationKey);
    if (relationIndex === undefined) {
      return false;
    }

    const relationObjectToRemove = this.manager.arbiter.relations[relationIndex];
    

    // --- Proceed with actual removal logic ---
    const relations = this.manager.arbiter.relations;
    const lastIndex = relations.length - 1;
    if (relationIndex !== lastIndex) {
      const lastRelation = relations[lastIndex];
      relations[relationIndex] = lastRelation;
      const lastKey = this.manager._makeRelationKey(lastRelation.src, lastRelation.rel, lastRelation.dst);
      this.manager._relationKeyToIndex.set(lastKey, relationIndex);
    }
    relations.pop();
    this.manager._relationsChanged = true;

    // Remove from Set for O(1) duplicate detection
    this.manager._relationKeys.delete(relationKey);
    this.manager._relationKeyToIndex.delete(relationKey);

    // Invalidate affected caches
    this.manager._invalidateRelationCaches(srcKey, relationName, dstKey);
    this.manager._invalidateValueRelationCaches(srcId, dstId, relationName);

    this.manager._removeRelationFromGraph(srcId, relationName, dstId);

    // Notify ValueManager about the removed relation's last state
    if (relationObjectToRemove.value !== undefined && relationObjectToRemove.stateId) {
      const relationUniqueKey = this.manager._makeCompositeKeyFromStrings(relationObjectToRemove.src, relationObjectToRemove.rel, relationObjectToRemove.dst);
      this.manager.arbiter.valueManager.invalidateCache([{
         relationUniqueKey: relationUniqueKey, 
         oldStateId: relationObjectToRemove.stateId, 
         accessTime: Date.now()
      }]);
    }

    // Invalidate relevant caches
    if (this.manager.arbiter.traversal) {
      this.manager.arbiter.traversal.invalidateAdjacencyCache();
    }
    if (this.manager.arbiter._invalidateDirectCheckCache) {
        this.manager.arbiter._invalidateDirectCheckCache(srcKey, relationName, dstKey);
    }
    // Embedding/similarity updates disabled
    
    // Update graph indices
    if (this.manager.arbiter.indices && typeof this.manager.arbiter.indices.removeRelation === 'function') {
        this.manager.arbiter.indices.removeRelation(relationObjectToRemove, srcId, dstId, relationName);
    }
    if (this.manager.arbiter.indices && typeof this.manager.arbiter.indices.updateAdjacencyAfterRemoval === 'function') {
        this.manager.arbiter.indices.updateAdjacencyAfterRemoval(srcId, dstId, this.manager.arbiter.relations);
    }
    
    // Update PLTC indices incrementally (if initialized and not in batch mode)
    if (!this.manager.arbiter.batchUpdateInProgress) {
      this.manager._updatePLTCIndicesOnRemove(srcId, dstId, relationName);
    }
    
    // Embedding/similarity updates disabled
    
    return true;
  }

  addRelationsBatch(relations, chunkSize = 5000) {
    if (this.manager.arbiter._snapshotReadOnly) {
      throw new Error('Cannot add relations in batch while in snapshot read-only mode. Disable snapshot before writing.');
    }
    let relationCount = 0;
    this.manager._batchOperationCount++;
    
    // Collect relation IDs for cache invalidation
    const valueRelationIds = [];
    const cacheInvalidationSet = new Set();
    
    for (let i = 0; i < relations.length; i += chunkSize) {
      const chunk = relations.slice(i, i + chunkSize);
      
      this.manager.arbiter.batchUpdateInProgress = true;
      // Embedding/similarity updates disabled
      
      try {
        for (const { srcKey, relation, dstKey, options } of chunk) {
          const srcId = this.manager.arbiter.nodeIdByKey.get(srcKey);
          const dstId = this.manager.arbiter.nodeIdByKey.get(dstKey);
          if (srcId !== undefined && dstId !== undefined) {
            const directKey = this.manager._makeCompositeKeyFromStrings(srcKey, relation, dstKey);
            valueRelationIds.push(directKey);
            // Track for cache invalidation using string-based keys
            const srcRelKey = this.manager._makeSrcRelCacheKeyFromStrings(srcKey, relation);
            const dstRelKey = this.manager._makeDstRelCacheKeyFromStrings(dstKey, relation);
            cacheInvalidationSet.add(srcRelKey);
            cacheInvalidationSet.add(dstRelKey);
            cacheInvalidationSet.add(directKey);
          }
          this.manager._addRelationInternal(srcKey, relation, dstKey, options);
          relationCount++;
        }
        
        // Embedding/similarity updates disabled
      } finally {
        this.manager.arbiter.batchUpdateInProgress = false;
        // Embedding/similarity updates disabled
      }
    }
    
    // Batch invalidate caches for better performance
    this.manager._batchInvalidateCaches(cacheInvalidationSet);

    // Invalidate the authorization direct-check cache for every touched
    // relation. The single-add path does this per relation; the batch path
    // previously skipped it, so cached grants/denies (including relation-
    // override entries via the dependency index) went stale after batch
    // loads — e.g. a deny cached before the batch survived the batch add.
    const touchedRelations = new Set();
    for (const { relation } of relations) {
      if (relation) touchedRelations.add(relation);
    }
    for (const rel of touchedRelations) {
      this.manager.arbiter._invalidateDirectCheckCache(null, rel, null);
    }
    
    // Invalidate value cache for all added value relations
    if (valueRelationIds.length > 0) {
      // this.manager.arbiter.valueManager.invalidateCache(valueRelationIds); 
    }
    
    // Cleanup caches periodically during heavy batch operations
    if (this.manager._batchOperationCount % 10 === 0) {
      this.manager._cleanupCaches();
    }
    
    // After batch operations complete, update PLTC indices for all added relations
    const reachabilityChecker = this.manager.arbiter.reachabilityChecker;
    if (reachabilityChecker && 
        (reachabilityChecker.pltcIndex?.initialized || 
         reachabilityChecker.pltcBackwardIndex?.initialized)) {
      // Extract unique edges from the batch and update PLTC indices
      const edgesToAdd = new Set();
      for (const { srcKey, dstKey } of relations) {
        const srcId = this.manager.arbiter.nodeIdByKey.get(srcKey);
        const dstId = this.manager.arbiter.nodeIdByKey.get(dstKey);
        if (srcId !== undefined && dstId !== undefined) {
          edgesToAdd.add(`${srcId}:${dstId}`);
        }
      }
      
      // Update PLTC indices for all unique edges
      // Note: For batch operations, we don't have relation names per edge, so we'll
      // trigger materialization updates after the batch completes
      reachabilityChecker.beginUpdateBatch();
      for (const edgeKey of edgesToAdd) {
        const [srcId, dstId] = edgeKey.split(':').map(Number);
        this.manager._updatePLTCIndicesOnAdd(srcId, dstId, null);
      }
      reachabilityChecker.commitUpdateBatch();
      
      // After batch, trigger materialization updates for all ChainRules
      // Collect all relation types used in the batch
      const batchRelationTypes = new Set();
      for (const { relation } of relations) {
        if (relation) {
          batchRelationTypes.add(relation);
        }
      }
      
      // Re-materialize ChainRules that use any of the relation types in the batch
      if (this.manager.arbiter.reachabilityChecker && batchRelationTypes.size > 0) {
        for (const [chainRuleName, dependencies] of this.manager.arbiter.reachabilityChecker.chainRuleDependencies?.entries() || []) {
          // Check if any batch relation type is a dependency
          let needsRematerialization = false;
          for (const batchRel of batchRelationTypes) {
            if (dependencies.has(batchRel)) {
              needsRematerialization = true;
              break;
            }
          }
          
          // No re-materialization needed - PLTC handles incremental updates automatically
        }
      }
    }
  }

  _addRelationInternal(srcKey, relation, dstKey, options = {}) {
    if (this.manager.arbiter._snapshotReadOnly) {
      throw new Error('Cannot add relation while in snapshot read-only mode. Disable snapshot before writing.');
    }
    const srcId = this.manager.arbiter.nodeIdByKey.get(srcKey);
    const dstId = this.manager.arbiter.nodeIdByKey.get(dstKey);
    if (srcId === undefined || dstId === undefined) return;
    
    const currentTime = Date.now();
    const newStateId = generateStateId();
    const relationKey = this.manager._makeRelationKey(srcId, relation, dstId);
    
    // Check if relation already exists via O(1) Map lookup instead of O(n) scan
    let existingRelation = null;
    const existingIndex = this.manager._relationKeyToIndex.get(relationKey);
    if (existingIndex !== undefined) {
      existingRelation = this.manager.arbiter.relations[existingIndex];
    }

    const relationObj = {
      src: srcId,
      rel: relation,
      dst: dstId,
      possibility: options.possibility !== undefined ? validatePossibility(options.possibility) : 1.0,
      reliability: options.reliability !== undefined ? options.reliability : 1.0,
      value: options.value,
      decayConfig: options.decayConfig,
      updated_last_at: currentTime,
      changed_last_at: (!existingRelation || 
                         options.value !== existingRelation.value || 
                         (options.reliability !== undefined && options.reliability !== existingRelation.reliability) || 
                         (options.possibility !== undefined && options.possibility !== existingRelation.possibility)) 
                       ? currentTime 
                       : existingRelation.changed_last_at,
      stateId: newStateId
    };

    if (existingRelation) {
      // This is an update, handle value staleness for the old state
      if (existingRelation.value !== undefined && existingRelation.stateId) {
        if (options.value !== existingRelation.value || 
            (options.reliability !== undefined && options.reliability !== existingRelation.reliability) || 
            (options.possibility !== undefined && options.possibility !== existingRelation.possibility)) {
          const relationUniqueKey = this.manager._makeCompositeKeyFromStrings(srcKey, relation, dstKey);
          this.manager.arbiter.valueManager.invalidateCache([{ 
            relationUniqueKey: relationUniqueKey, 
            oldStateId: existingRelation.stateId,
            accessTime: existingRelation.changed_last_at || existingRelation.updated_last_at
          }]);
        }
      }
      
      // Register the tuple identity so removeRelation/_modifyRelation can
      // find it later (batch-added relations were previously invisible to
      // _relationKeys/_relationKeyToIndex, making them immutable).
      this.manager._relationKeys.add(relationKey);
      this.manager._relationKeyToIndex.set(relationKey, existingIndex);

      // Remove old relation from indices (if it exists in indices)
      if (this.manager.arbiter.indices) {
        this.manager.arbiter.indices.removeRelation(existingRelation); // Remove old from index
      }
      
      // Update relation in main relations array using pre-computed index
      this.manager.arbiter.relations[existingIndex] = relationObj;
    } else {
      this.manager.arbiter.relations.push(relationObj);
      // Register the tuple identity (batch path: _addRelationInternal is
      // also used by addRelationsBatch, which must keep relations mutable
      // — removeRelation/_modifyRelation look these maps up).
      this.manager._relationKeys.add(relationKey);
      this.manager._relationKeyToIndex.set(relationKey, this.manager.arbiter.relations.length - 1);
    }

    this.manager._relationsChanged = true;
    this.manager._invalidateRelationCaches(srcId, relation, dstId);

    this.manager._addRelationToGraph(srcId, relation, dstId);
    
    // Embedding/similarity updates disabled

    this.manager.arbiter.indices.addRelation(relationObj); // Add new/updated to index
    this.manager.arbiter.indicesBuilt = true;

    this.manager._addRelationToGraph(srcId, relation, dstId);
    
    // Update PLTC indices incrementally (if initialized and not in batch mode)
    // For updates, only update if it's a new relation (not an existing one being modified)
    if (!this.manager.arbiter.batchUpdateInProgress && !existingRelation) {
      this.manager._updatePLTCIndicesOnAdd(srcId, dstId, relation);
    }
    
    // Embedding/similarity updates disabled
  }

  updateRelationsBatch(updates, chunkSize = 5000) {
    if (this.manager.arbiter._snapshotReadOnly) {
      throw new Error('Cannot update relations in batch while in snapshot read-only mode. Disable snapshot before writing.');
    }

    // Validate every operation up front so a bad op cannot partially apply.
    for (const op of updates) {
      if (!op || (op.operation !== 'add' && op.operation !== 'remove' && op.operation !== 'modify')) {
        throw new Error(`Unknown operation: ${op.operation}`);
      }
    }

    // Apply operations strictly in the given order. The previous
    // remove->modify->add pre-sort changed the final state whenever one
    // tuple was touched by mixed kinds: [add, remove] left the tuple
    // present, and [modify, add, modify] ended with the middle value
    // instead of the last one (last-write-wins).
    const arbiter = this.manager.arbiter;
    arbiter.batchUpdateInProgress = true;
    try {
      for (let i = 0; i < updates.length; i += chunkSize) {
        const chunk = updates.slice(i, i + chunkSize);
        for (const op of chunk) {
          switch (op.operation) {
            case 'add':
              this._addRelationInternal(op.srcKey, op.relation, op.dstKey, op.options);
              break;
            case 'remove':
              this.manager.removeRelation(op.srcKey, op.relation, op.dstKey);
              break;
            case 'modify':
              this._modifyRelation(op.srcKey, op.relation, op.dstKey, op.options);
              break;
          }
        }
      }
    } finally {
      arbiter.batchUpdateInProgress = false;
    }

    // After the batch completes, update PLTC indices for all touched edges
    // (adds and removes both affect reachability).
    const reachabilityChecker = arbiter.reachabilityChecker;
    if (reachabilityChecker &&
        (reachabilityChecker.pltcIndex?.initialized ||
         reachabilityChecker.pltcBackwardIndex?.initialized)) {
      const edgesToAdd = new Set();
      for (const op of updates) {
        const srcId = arbiter.nodeIdByKey.get(op.srcKey);
        const dstId = arbiter.nodeIdByKey.get(op.dstKey);
        if (srcId !== undefined && dstId !== undefined) {
          edgesToAdd.add(`${srcId}:${dstId}`);
        }
      }
      reachabilityChecker.beginUpdateBatch();
      for (const edgeKey of edgesToAdd) {
        const [srcId, dstId] = edgeKey.split(':').map(Number);
        this.manager._updatePLTCIndicesOnAdd(srcId, dstId, null);
      }
      reachabilityChecker.commitUpdateBatch();
    }

    // The batch path bypasses Arbiter.addRelation/removeRelation, so the
    // arbiter-level rule caches (rule result cache, ChainRule caches,
    // direct-check cache) are never invalidated there. A query that warmed
    // those caches before the batch would keep serving stale decisions.
    const affectedRelations = new Set();
    for (const op of updates) {
      if (op && op.relation !== undefined) affectedRelations.add(op.relation);
    }
    for (const relation of affectedRelations) {
      this.manager.arbiter._invalidateDirectCheckCache(null, relation, null);
      this.manager.arbiter.invalidateRuleResultCacheByRelation(relation);
      if (this.manager.arbiter.authChecker) {
        this.manager.arbiter.authChecker.invalidateRuleCaches(relation);
      }
    }

    return this.manager;
  }

  _processBatchRemovals(removeOps, chunkSize) {
    if (removeOps.length === 0) return;
    
    // Collect relation IDs for cache invalidation
    const valueRelationIds = [];
    const cacheInvalidationSet = new Set();
    
    for (var i = 0; i < removeOps.length; i += chunkSize) {
      const chunk = removeOps.slice(i, i + chunkSize);
      
      this.manager.arbiter.batchUpdateInProgress = true;
      // Embedding/similarity updates disabled
      
      try {
        var chunkLen = chunk.length;
        for (var j = 0; j < chunkLen; j++) {
          const op = chunk[j];
          const srcId = this.manager.arbiter.nodeIdByKey.get(op.srcKey);
          const dstId = this.manager.arbiter.nodeIdByKey.get(op.dstKey);
          if (srcId !== undefined && dstId !== undefined) {
            const directKey = this.manager._makeCompositeKeyFromStrings(op.srcKey, op.relation, op.dstKey);
            valueRelationIds.push(directKey);
            // Track for cache invalidation using string-based keys
            const srcRelKey = this.manager._makeSrcRelCacheKeyFromStrings(op.srcKey, op.relation);
            const dstRelKey = this.manager._makeDstRelCacheKeyFromStrings(op.dstKey, op.relation);
            cacheInvalidationSet.add(srcRelKey);
            cacheInvalidationSet.add(dstRelKey);
            cacheInvalidationSet.add(directKey);
          }
          this.manager.removeRelation(op.srcKey, op.relation, op.dstKey);
        }
        
        // Embedding/similarity updates disabled
      } finally {
        this.manager.arbiter.batchUpdateInProgress = false;
        // Embedding/similarity updates disabled
      }
    }
    
    // Batch invalidate caches
    this.manager._batchInvalidateCaches(cacheInvalidationSet);
    
    // Invalidate value cache
    if (valueRelationIds.length > 0) {
      // this.manager.arbiter.valueManager.invalidateCache(valueRelationIds); 
    }
  }

  _processBatchAdditions(addOps, chunkSize) {
    if (addOps.length === 0) return;
    
    const relations = [];
    var len = addOps.length;
    for (var i = 0; i < len; i++) {
      const op = addOps[i];
      relations[i] = {
        srcKey: op.srcKey,
        relation: op.relation,
        dstKey: op.dstKey,
        options: op.options
      };
    }
    
    this.manager.addRelationsBatch(relations, chunkSize);
  }

  _processBatchModifications(modifyOps, chunkSize) {
    if (modifyOps.length === 0) return;
    
    // Collect relation IDs for cache invalidation
    const valueRelationIds = [];
    const cacheInvalidationSet = new Set();
    
    for (var i = 0; i < modifyOps.length; i += chunkSize) {
      const chunk = modifyOps.slice(i, i + chunkSize);
      
      this.manager.arbiter.batchUpdateInProgress = true;
      // Embedding/similarity updates disabled
      
      try {
        var chunkLen = chunk.length;
        for (var j = 0; j < chunkLen; j++) {
          const op = chunk[j];
          const srcId = this.manager.arbiter.nodeIdByKey.get(op.srcKey);
          const dstId = this.manager.arbiter.nodeIdByKey.get(op.dstKey);
          if (srcId !== undefined && dstId !== undefined) {
            if (op.options?.value !== undefined || op.options?.possibility !== undefined) {
              const directKey = this.manager._makeCompositeKeyFromStrings(op.srcKey, op.relation, op.dstKey);
              valueRelationIds.push(directKey);
            }
            // Track for cache invalidation using string-based keys
            const srcRelKey = this.manager._makeSrcRelCacheKeyFromStrings(op.srcKey, op.relation);
            const dstRelKey = this.manager._makeDstRelCacheKeyFromStrings(op.dstKey, op.relation);
            const directKey = this.manager._makeCompositeKeyFromStrings(op.srcKey, op.relation, op.dstKey);
            cacheInvalidationSet.add(srcRelKey);
            cacheInvalidationSet.add(dstRelKey);
            cacheInvalidationSet.add(directKey);
          }
          this._modifyRelation(op.srcKey, op.relation, op.dstKey, op.options);
        }
        
        // Embedding/similarity updates disabled
      } finally {
        this.manager.arbiter.batchUpdateInProgress = false;
        // Embedding/similarity updates disabled
      }
    }
    
    // Batch invalidate caches
    this.manager._batchInvalidateCaches(cacheInvalidationSet);
    
    // Invalidate value cache
    if (valueRelationIds.length > 0) {
      // this.manager.arbiter.valueManager.invalidateCache(valueRelationIds); 
    }
  }

  _modifyRelation(srcKey, relation, dstKey, newOptions) {
    if (this.manager.arbiter._snapshotReadOnly) {
      throw new Error('Cannot modify relation while in snapshot read-only mode. Disable snapshot before writing.');
    }
    const srcId = this.manager.arbiter.nodeIdByKey.get(srcKey);
    const dstId = this.manager.arbiter.nodeIdByKey.get(dstKey);
    if (srcId === undefined || dstId === undefined) return false;

    this.manager._ensureIndicesBuilt();
    const oldRelation = this.manager.arbiter.indices.getDirectRelation(srcId, relation, dstId);
    if (!oldRelation) return false;
    const oldStateId = oldRelation.stateId;
    const oldValue = oldRelation.value;
    const oldReliability = oldRelation.reliability;
    const oldPossibility = oldRelation.possibility;

    const currentTime = Date.now();
    const newStateId = generateStateId();

    oldRelation.possibility = newOptions?.possibility !== undefined ? validatePossibility(newOptions.possibility) : oldRelation.possibility;
    oldRelation.reliability = newOptions?.reliability !== undefined ? newOptions.reliability : oldRelation.reliability;
    oldRelation.value = newOptions?.value !== undefined ? newOptions.value : oldRelation.value;
    oldRelation.decayConfig = newOptions?.decayConfig !== undefined ? newOptions.decayConfig : oldRelation.decayConfig;
    oldRelation.validity = newOptions?.validity !== undefined ? newOptions.validity : oldRelation.validity;
    oldRelation.updated_last_at = currentTime;
    // Honor an explicit changed_last_at override like the add path does,
    // but only on writes that would refresh anyway: a write that does not
    // change the value (or its interpretation) keeps the old timestamp,
    // so replaying identical tuples never un-expires old values.
    const shouldRefresh = newOptions?.value !== oldValue ||
      (newOptions?.reliability !== undefined && newOptions.reliability !== oldReliability) ||
      (newOptions?.possibility !== undefined && newOptions.possibility !== oldPossibility);
    oldRelation.changed_last_at = shouldRefresh
      ? (newOptions?.changed_last_at !== undefined ? newOptions.changed_last_at : currentTime)
      : oldRelation.changed_last_at;
    oldRelation.stateId = newStateId;
    this.manager._relationsChanged = true;
    this.manager._invalidateRelationCaches(srcId, relation, dstId);
    this.manager._invalidateValueRelationCaches(srcId, dstId, relation);

    // If the value or its interpretation (reliability/possibility) changed, invalidate the old value state
    if (oldValue !== undefined && oldStateId) {
      if (newOptions?.value !== oldValue || 
          (newOptions?.reliability !== undefined && newOptions.reliability !== oldReliability) ||
          (newOptions?.possibility !== undefined && newOptions.possibility !== oldPossibility)) {
        const relationUniqueKey = this.manager._makeCompositeKeyFromStrings(srcKey, relation, dstKey);
        this.manager.arbiter.valueManager.invalidateCache([{
          relationUniqueKey: relationUniqueKey,
          oldStateId: oldStateId, 
          accessTime: oldRelation.changed_last_at || oldRelation.updated_last_at
        }]);
      }
    }

    // Embedding/similarity updates disabled
    
    return true;
  }

  updateRelationsBatchTransactional(updates) {
    if (this.manager.arbiter._snapshotReadOnly) {
      throw new Error('Cannot update relations in transactional batch while in snapshot read-only mode. Disable snapshot before writing.');
    }
    const originalRelations = this.manager.arbiter.relations.map(r => ({ ...r }));
    const originalIndices = {
      relationsBySrcRelDst: new Map(this.manager.arbiter.indices.relationsBySrcRelDst),
      relationsBySrcRel: new Map(this.manager.arbiter.indices.relationsBySrcRel),
      relationsByDstRel: new Map(this.manager.arbiter.indices.relationsByDstRel),
      relationsByRel: new Map(this.manager.arbiter.indices.relationsByRel),
      outgoingEdges: new Map(this.manager.arbiter.indices.outgoingEdges),
      incomingEdges: new Map(this.manager.arbiter.indices.incomingEdges)
    };
    const originalRelationKeys = new Set(this.manager._relationKeys);
    const originalRelationKeyToIndex = new Map(this.manager._relationKeyToIndex);
    
    try {
      this.updateRelationsBatch(updates);
      return { success: true, error: null };
    } catch (error) {
      this.manager.arbiter.relations = originalRelations;
      this.manager._relationKeys = originalRelationKeys;
      this.manager._relationKeyToIndex = originalRelationKeyToIndex;
      
      // Rebuild indices from restored relations to guarantee consistency
      this.manager.arbiter.indices.relationsBySrcRelDst.clear();
      this.manager.arbiter.indices.relationsBySrcRel.clear();
      this.manager.arbiter.indices.relationsByDstRel.clear();
      this.manager.arbiter.indices.relationsByRel.clear();
      this.manager.arbiter.indices.outgoingEdges.clear();
      this.manager.arbiter.indices.incomingEdges.clear();
      for (const rel of originalRelations) {
        this.manager.arbiter.indices.addRelation(rel);
      }
      
      // Clear all caches on rollback
      this.clearAllCaches();
      this.manager.arbiter.valueManager.invalidateCache();
      
      return { success: false, error: error.message };
    }
  }

  clearAllCaches() {
    this.manager._clearAllCaches();
  }
}
