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

export class NodeManager {
  constructor(arbiter, options = {}) {
    this.arbiter = arbiter;
    // DecisionCache port — RF-04 closure. The cache-leak fix moves out
    // of NodeManager so this file no longer reaches into private cache
    // internals. When no DecisionCache is injected, default to one that
    // forwards to the arbiter (preserves existing behavior).
    if (options.decisionCache) {
      this.decisionCache = options.decisionCache;
    } else {
      // Lazy require to avoid a circular import with AuthorizationChecker
      // (DecisionCache imports Arbiter's caches, but Arbiter imports
      // NodeManager which would otherwise import DecisionCache back).
      // Falling back to undefined tells updateNodeData to use the legacy
      // arbiter-field path; tests that want the new path can inject.
      this.decisionCache = null;
    }
  }

  addNode(key, type, data = {}) {
    if (this.arbiter.nodeIdByKey.has(key)) return this.arbiter.nodeIdByKey.get(key);
    
    const nodeId = this.arbiter.nextNodeId++;
    this.arbiter.nodeIdByKey.set(key, nodeId);
    this.arbiter.keyByNodeId.set(nodeId, key);
    
    const currentTime = Date.now();
    const newSateId = generateStateId();

    this.arbiter.nodes.set(nodeId, { 
      key, 
      type, 
      data, 
      embedding: null, 
      stale: true,
      reliability: data.reliability !== undefined ? data.reliability : 1.0,
      updated_last_at: currentTime,
      changed_last_at: currentTime,
      stateId: newSateId
    });
    
    if (this.arbiter.embeddingManager) {
      this.arbiter.embeddingManager.markNodeStale(nodeId);
    }
    if (this.arbiter.similarityManager) {
      this.arbiter.similarityManager.markNodeAsStaleForEmbedding(key, 'node_added', currentTime);
    }
    
    return nodeId;
  }

  addNodesBatch(nodes) {
    for (const { key, type, data } of nodes) {
      this.addNode(key, type, data);
    }
  }

  getNode(nodeId) {
    return this.arbiter.nodes.get(nodeId);
  }

  getNodeByKey(key) {
    const nodeId = this.arbiter.nodeIdByKey.get(key);
    return nodeId !== undefined ? this.getNode(nodeId) : null;
  }

  getNodeId(key) {
    return this.arbiter.nodeIdByKey.get(key);
  }

  getNodeKey(nodeId) {
    return this.arbiter.keyByNodeId.get(nodeId);
  }

  removeNode(key) {
    const nodeId = this.arbiter.nodeIdByKey.get(key);
    if (nodeId === undefined) return false;
    
    // Cascade: remove all relations involving this node
    const relations = this.arbiter.relations;
    for (let i = relations.length - 1; i >= 0; i--) {
      const rel = relations[i];
      if (rel.src === nodeId || rel.dst === nodeId) {
        const srcKey = this.arbiter.keyByNodeId.get(rel.src);
        const dstKey = this.arbiter.keyByNodeId.get(rel.dst);
        if (srcKey && dstKey) {
          this.arbiter.relationManager.removeRelation(srcKey, rel.rel, dstKey);
        }
      }
    }
    
    this.arbiter.nodes.delete(nodeId);
    this.arbiter.nodeIdByKey.delete(key);
    this.arbiter.keyByNodeId.delete(nodeId);
    
    if (this.arbiter.embeddingManager) {
      this.arbiter.embeddingManager.removeNodeEmbedding(nodeId);
    }
    if (this.arbiter.similarityManager) {
      if (this.arbiter.similarityManager.staleNodesIndex.has(key)) {
        const staleItem = this.arbiter.similarityManager.staleNodesIndex.get(key);
        this.arbiter.similarityManager.staleNodesIndex.delete(key);
      }
      this.arbiter.similarityManager.removeNode(key);
    }
    
    return true;
  }

  updateNodeData(key, data) {
    const node = this.getNodeByKey(key);
    if (!node) return false;
    
    const currentTime = Date.now();
    Object.assign(node.data, data);
    node.updated_last_at = currentTime;
    node.stateId = generateStateId();
    node.stale = true;
    
    if (data.reliability !== undefined) {
      node.reliability = data.reliability;
    }

    // Invalidate caches that might contain this node
    if (this.decisionCache) {
      // New path: port-driven cache invalidation. The cache file is the
      // only place that knows its own storage layout, so the leaky
      // string-pattern walk stays out of NodeManager.
      this.decisionCache.invalidateByNodeKey(key);
    } else {
      // Legacy path: direct arbiter fields. Kept for backward compat
      // until every call site wires DecisionCache.
      this._invalidateNodeCaches(key);
    }
    // Invalidate rule result cache entries for relations involving this node,
    // rather than flushing the entire cache
    if (this.arbiter.dependencyIndex && this.arbiter.dependencyIndex.size > 0) {
      const nodeId = this.arbiter.nodeIdByKey.get(key);
      if (nodeId !== undefined) {
        for (const [relation] of this.arbiter.dependencyIndex) {
          if (this.decisionCache) {
            this.decisionCache.invalidateByRelation(relation);
          } else {
            this.arbiter.invalidateRuleResultCacheByRelation(relation);
          }
        }
      }
    }

    if (this.arbiter.embeddingManager) {
      const nodeId = this.arbiter.nodeIdByKey.get(key);
      if (nodeId !== undefined) this.arbiter.embeddingManager.markNodeStale(nodeId);
    }
    if (this.arbiter.similarityManager) {
      this.arbiter.similarityManager.markNodeAsStaleForEmbedding(key, 'node_data_changed', currentTime);
    }
    
    return true;
  }

  getAllNodes() {
    return Array.from(this.arbiter.nodes.values());
  }

  getAllNodeKeys() {
    return Array.from(this.arbiter.nodeIdByKey.keys());
  }

  getNodeCount() {
    return this.arbiter.nodes.size;
  }

  clearNodes() {
    this.arbiter.nodes.clear();
    this.arbiter.nodeIdByKey.clear();
    this.arbiter.keyByNodeId.clear();
    this.arbiter.nextNodeId = 0;
  }

  /**
   * Invalidate caches that might contain data for a specific node
   * @private
   */
  _invalidateNodeCaches(nodeKey) {
    // Direct check cache: iterate and remove entries matching this nodeKey.
    // HyperbolicLRUCache has no pattern-based invalidation; use key-level removal.
    if (this.arbiter.directCheckCache && typeof this.arbiter.directCheckCache.delete === 'function') {
      const nodeId = this.arbiter.nodeIdByKey.get(nodeKey);
      if (nodeId !== undefined) {
        // Direct check cache keys are composite strings; iterate to find matches
        if (this.arbiter.directCheckCache._cache && typeof this.arbiter.directCheckCache._cache === 'object') {
          const entries = this.arbiter.directCheckCache._cache instanceof Map
            ? [...this.arbiter.directCheckCache._cache]
            : Object.entries(this.arbiter.directCheckCache._cache);
          for (const [cacheKey] of entries) {
            if (typeof cacheKey === 'string' && cacheKey.includes(String(nodeId))) {
              this.arbiter.directCheckCache.delete(cacheKey);
            }
          }
        }
      }
    }
  }

} 
