
import { UnifiedKeyManager } from './UnifiedKeyManager.js';

export class GraphIndices {
  constructor() {
    // Initialize unified key manager
    this.keyManager = new UnifiedKeyManager();
    
    this.relationsBySrcRelDst = new Map();
    this.relationsBySrcRel = new Map();
    this.relationsByDstRel = new Map();
    this.relationsByRel = new Map();
    this.outgoingEdges = new Map();
    this.incomingEdges = new Map();
  }

  clear() {
    this.relationsBySrcRelDst.clear();
    this.relationsBySrcRel.clear();
    this.relationsByDstRel.clear();
    this.relationsByRel.clear();
    this.outgoingEdges.clear();
    this.incomingEdges.clear();
    this.keyManager.clear();
  }

  // Get or create numeric ID for relation name
  _getRelationId(relationName) {
    return this.keyManager._getRelationId(relationName);
  }

  // Safe composite key generation — collision-free via the UnifiedKeyManager string interning.
  // Replaces previous XOR-hash approach that truncated to 32 bits and was collision-prone.
  _makeDirectKey(srcId, relationName, dstId) {
    return this.keyManager.createCompositeKey(srcId, relationName, dstId);
  }

  _makeSrcRelKey(srcId, relationName) {
    return this.keyManager.createCompositeKey(srcId, relationName);
  }

  _makeDstRelKey(dstId, relationName) {
    return this.keyManager.createCompositeKey(dstId, relationName);
  }

  addRelation(relationObj) {
    const { src: srcId, rel: relation, dst: dstId } = relationObj;

    const directKey = this._makeDirectKey(srcId, relation, dstId);
    // Idempotence at the index level: a (src, rel, dst) tuple is the canonical
    // identity for a relation. relationsBySrcRelDst dedups on this composite key,
    // but relationsBySrcRel / relationsByDstRel / relationsByRel use Set.add()
    // with reference identity — without this guard, two distinct objects with
    // the same key would leave the four indexes inconsistent. RelationManager
    // already routes duplicates to _modifyRelation, but GraphIndices is also
    // reachable via buildIndices(relations) where the relations array may
    // contain duplicates, and via direct callers (EmbeddingManager.tfidfFilter,
    // UnifiedKeyManager.buildIndices). RF-22 closure.
    //
    // When a duplicate is encountered, replace the existing entry: the new
    // relationObj supersedes the old one in the composite-key index, and the
    // old object is dropped from the bySrcRel/byDstRel/byRel Sets before the
    // new object is added. This keeps all four indexes consistent and matches
    // the last-writer-wins semantics of relationsBySrcRelDst.
    if (this.relationsBySrcRelDst.has(directKey)) {
      const oldObj = this.relationsBySrcRelDst.get(directKey);
      if (oldObj !== relationObj) {
        const srcRelKey = this._makeSrcRelKey(srcId, relation);
        const dstRelKey = this._makeDstRelKey(dstId, relation);
        this.relationsBySrcRel.get(srcRelKey)?.delete(oldObj);
        this.relationsByDstRel.get(dstRelKey)?.delete(oldObj);
        this.relationsByRel.get(relation)?.delete(oldObj);
      }
    }
    this.relationsBySrcRelDst.set(directKey, relationObj);
    
    const srcRelKey = this._makeSrcRelKey(srcId, relation);
    let srcRelSet = this.relationsBySrcRel.get(srcRelKey);
    if (!srcRelSet) {
      srcRelSet = new Set();
      this.relationsBySrcRel.set(srcRelKey, srcRelSet);
    }
    srcRelSet.add(relationObj);
    
    const dstRelKey = this._makeDstRelKey(dstId, relation);
    let dstRelSet = this.relationsByDstRel.get(dstRelKey);
    if (!dstRelSet) {
      dstRelSet = new Set();
      this.relationsByDstRel.set(dstRelKey, dstRelSet);
    }
    dstRelSet.add(relationObj);
    
    let relSet = this.relationsByRel.get(relation);
    if (!relSet) {
      relSet = new Set();
      this.relationsByRel.set(relation, relSet);
    }
    relSet.add(relationObj);
    
    let outgoing = this.outgoingEdges.get(srcId);
    if (!outgoing) {
      outgoing = [];
      this.outgoingEdges.set(srcId, outgoing);
    }
    outgoing.push(dstId);
    
    let incoming = this.incomingEdges.get(dstId);
    if (!incoming) {
      incoming = [];
      this.incomingEdges.set(dstId, incoming);
    }
    incoming.push(srcId);



  }

  removeRelation(relationObj, srcId, dstId, relation) {
    const directKey = this._makeDirectKey(srcId, relation, dstId);
    // Look up the actual stored object — the caller's `relationObj` may be a
    // stale reference (different object with the same (src, rel, dst) tuple).
    // Without this lookup, the Set-backed indexes would retain the original
    // entry even though `relationsBySrcRelDst` is correctly cleared.
    // RelationUpdates.removeRelation looks up the actual stored object from
    // arbiter.relations before calling this method, but GraphIndices is also
    // reachable directly (buildIndices, EmbeddingManager.tfidfFilter) where
    // the caller may pass any reference. RF-23 closure (parallel to RF-22
    // for addRelation).
    const storedObj = this.relationsBySrcRelDst.get(directKey);
    if (!storedObj) return;  // nothing to remove
    this.relationsBySrcRelDst.delete(directKey);

    const srcRelKey = this._makeSrcRelKey(srcId, relation);
    const srcRelSet = this.relationsBySrcRel.get(srcRelKey);
    if (srcRelSet) {
      srcRelSet.delete(storedObj);
      if (srcRelSet.size === 0) {
        this.relationsBySrcRel.delete(srcRelKey);
      }
    }

    const dstRelKey = this._makeDstRelKey(dstId, relation);
    const dstRelSet = this.relationsByDstRel.get(dstRelKey);
    if (dstRelSet) {
      dstRelSet.delete(storedObj);
      if (dstRelSet.size === 0) {
        this.relationsByDstRel.delete(dstRelKey);
      }
    }

    const relSet = this.relationsByRel.get(relation);
    if (relSet) {
      relSet.delete(storedObj);
      if (relSet.size === 0) {
        this.relationsByRel.delete(relation);
      }
    }
  }

  updateAdjacencyAfterRemoval(srcId, dstId, relations) {
    const hasOtherOutgoing = relations.some(r => r.src === srcId && r.dst === dstId);
    if (!hasOtherOutgoing) {
      const outgoing = this.outgoingEdges.get(srcId);
      if (outgoing) {
        if (outgoing.length === 1) {
          this.outgoingEdges.delete(srcId);
        } else {
          for (let i = 0; i < outgoing.length; i++) {
            if (outgoing[i] === dstId) {
              const lastIndex = outgoing.length - 1;
              if (i !== lastIndex) {
                outgoing[i] = outgoing[lastIndex];
              }
              outgoing.pop();
              break;
            }
          }
          if (outgoing.length === 0) {
            this.outgoingEdges.delete(srcId);
          }
        }
      }
    }

    const hasOtherIncoming = relations.some(r => r.src === srcId && r.dst === dstId);
    if (!hasOtherIncoming) {
      const incoming = this.incomingEdges.get(dstId);
      if (incoming) {
        if (incoming.length === 1) {
          this.incomingEdges.delete(dstId);
        } else {
          for (let i = 0; i < incoming.length; i++) {
            if (incoming[i] === srcId) {
              const lastIndex = incoming.length - 1;
              if (i !== lastIndex) {
                incoming[i] = incoming[lastIndex];
              }
              incoming.pop();
              break;
            }
          }
          if (incoming.length === 0) {
            this.incomingEdges.delete(dstId);
          }
        }
      }
    }
  }

  getDirectRelation(srcId, relation, dstId) {
    const key = this._makeDirectKey(srcId, relation, dstId);
    return this.relationsBySrcRelDst.get(key);
  }

  getRelationsFromSrc(srcId, relation) {
    const key = this._makeSrcRelKey(srcId, relation);
    const relations = this.relationsBySrcRel.get(key);
    if (!relations) return [];
    
    const result = [];
    const relArray = Array.from(relations);
    var len = relArray.length;
    for (var i = 0; i < len; i++) {
      result[i] = relArray[i];
    }
    return result;
  }

  getRelationsToDst(dstId, relation) {
    const key = this._makeDstRelKey(dstId, relation);
    const relations = this.relationsByDstRel.get(key);
    if (!relations) return [];
    
    const result = [];
    const relArray = Array.from(relations);
    var len = relArray.length;
    for (var i = 0; i < len; i++) {
      result[i] = relArray[i];
    }
    return result;
  }

  getRelationsByName(relation) {
    const relations = this.relationsByRel.get(relation);
    if (!relations) return [];
    
    const result = [];
    const relArray = Array.from(relations);
    var len = relArray.length;
    for (var i = 0; i < len; i++) {
      result[i] = relArray[i];
    }
    return result;
  }

  buildIndices(relations) {
    this.clear();
    
    for (const relationObj of relations) {
      this.addRelation(relationObj);
    }
  }
} 
