/**
 * UnifiedKeyManager - Agnostic universal key management system
 * 
 * This provides a unified API for string-to-ID conversion and composite key generation
 * across all managers in the system. It's completely agnostic to the domain and makes
 * no assumptions about what strings or relations will be used.
 */

export class UnifiedKeyManager {
  constructor(options = {}) {
    this.options = {
      // Optimized bit ranges for billion-scale systems
      maxSrcId: options.maxSrcId || 0x3FFFFFFF,     // 30 bits (1B+ nodes)
      maxRelationId: options.maxRelationId || 0xFFFF, // 16 bits (65K relations)  
      maxDstId: options.maxDstId || 0x3FFFFFFF,     // 30 bits (1B+ nodes)
      
      ...options
    };
    
    // String-to-ID mappings (using sequential IDs - the fastest approach)
    this.stringToId = new Map();
    this.idToString = new Map();
    this.nextId = 1;
    
    // Relation ID cache
    this.relationIdCache = new Map();
    this.nextRelationId = 1;
    
  }
  
  /**
   * Get or create relation ID
   * @param {string} relation - Relation name
   * @returns {number} Relation ID
   */
  _getRelationId(relation) {
    if (this.relationIdCache.has(relation)) {
      return this.relationIdCache.get(relation);
    }
    
    const id = this.nextRelationId++;
    this.relationIdCache.set(relation, id);
    
    // Validate ID range
    if (id > this.options.maxRelationId) {
      console.warn(`[UnifiedKeyManager] Relation ID ${id} exceeds max range ${this.options.maxRelationId}`);
    }
    
    return id;
  }
  
  /**
   * Convert string to ID using sequential assignment (fastest approach)
   * @param {string} str - Input string
   * @returns {number} Unique ID
   */
  getStringId(str) {
    if (this.stringToId.has(str)) {
      return this.stringToId.get(str);
    }
    
    const id = this.nextId++;
    this.stringToId.set(str, id);
    this.idToString.set(id, str);
    return id;
  }
  
  /**
   * Convert ID back to string
   * @param {number} id - ID to convert
   * @returns {string|null} Original string or null if not found
   */
  getIdString(id) {
    return this.idToString.get(id) || null;
  }
  
  /**
   * Create composite key using compact string encoding
   * @param {number} srcId - Source ID
   * @param {string} relation - Relation name
   * @param {number} dstId - Destination ID
   * @returns {string} Composite key
   */
  createCompositeKey(srcId, relation, dstId) {
    if (srcId > this.options.maxSrcId) {
      throw new Error(`Source ID ${srcId} exceeds max range ${this.options.maxSrcId}`);
    }
    if (dstId > this.options.maxDstId) {
      throw new Error(`Destination ID ${dstId} exceeds max range ${this.options.maxDstId}`);
    }

    const relationId = this._getRelationId(relation);
    // Rolling hash (no string concatenation). Exact-key caches recompute the
    // key to delete; the direct-check cache additionally tracks keys by
    // relation/node so relation-level invalidation does not need the
    // pipe-delimited string form.
    return this._rollingHash(srcId, relationId, dstId);
  }
  
  /**
   * Create source-relation key
   * @param {number} srcId - Source ID
   * @param {string} relation - Relation name
   * @returns {number} Rolling-hash key
   */
  createSrcRelKey(srcId, relation) {
    if (srcId > this.options.maxSrcId) {
      throw new Error(`Source ID ${srcId} exceeds max range ${this.options.maxSrcId}`);
    }
    const relationId = this._getRelationId(relation);
    // Direction marker keeps fromSrc and toDst lookups distinct (identical
    // srcId|relationId pairs previously shared keys across directions).
    return this._rollingHash(srcId, relationId, 0x73); // 's'
  }
  
  /**
   * Create destination-relation key
   * @param {number} dstId - Destination ID
   * @param {string} relation - Relation name
   * @returns {number} Rolling-hash key
   */
  createDstRelKey(dstId, relation) {
    if (dstId > this.options.maxDstId) {
      throw new Error(`Destination ID ${dstId} exceeds max range ${this.options.maxDstId}`);
    }
    const relationId = this._getRelationId(relation);
    return this._rollingHash(dstId, relationId, 0x64); // 'd'
  }
  
  /**
   * Create chain key with optimized step hashing
   * @param {number} userId - User ID
   * @param {number} objectId - Object ID
   * @param {Array} steps - Chain steps
   * @returns {number} Rolling-hash composite key
   */
  createChainKey(userId, objectId, steps) {
    if (userId > this.options.maxSrcId) {
      throw new Error(`User ID ${userId} exceeds max range ${this.options.maxSrcId}`);
    }
    if (objectId > this.options.maxDstId) {
      throw new Error(`Object ID ${objectId} exceeds max range ${this.options.maxDstId}`);
    }

    // Rolling hash instead of JSON.stringify: no string allocation, no
    // serialization of possibly-nested step configs. Steps may be strings,
    // { relation, direction } objects, or { rule: <config>, conditionStep }
    // objects — each feeds the hash incrementally.
    return this._rollingHash(userId, objectId, steps);
  }

  /**
   * 53-bit rolling hash over heterogeneous components (numbers, strings,
   * booleans, arrays, nested objects). Dual 32-bit FNV-1a lanes combined into
   * a single exact integer (< 2^53) usable as a Map key — no string
   * concatenation or serialization on the hot path. Integers feed their
   * high/low 32-bit halves; floats feed their exact 64-bit byte pattern;
   * objects feed their sorted key/value pairs so ordering is stable.
   * @returns {number} exact integer in [0, 2^53)
   */
  _rollingHash(...parts) {
    let h1 = 0x811c9dc5;
    let h2 = 0x9e3779b1;
    const floatBuf = new Float64Array(1);
    const floatBytes = new Uint8Array(floatBuf.buffer);
    const mix1 = (h, x) => Math.imul(h ^ x, 0x01000193) >>> 0;
    const mix2 = (h, x) => Math.imul((h ^ x) ^ 0x5bd1e995, 0x01000193) >>> 0;
    const feed = (v) => {
      if (typeof v === 'number') {
        if (Number.isInteger(v)) {
          const hi = Math.floor(v / 0x100000000);
          const lo = v >>> 0;
          h1 = mix1(h1, hi);
          h2 = mix2(h2, lo);
          h1 = mix1(h1, lo);
          h2 = mix2(h2, hi ^ 0x9e37);
        } else {
          floatBuf[0] = v;
          for (let i = 0; i < 8; i++) {
            h1 = mix1(h1, floatBytes[i]);
            h2 = mix2(h2, floatBytes[i]);
          }
        }
      } else if (typeof v === 'string') {
        h1 = mix1(h1, v.length | 0x8000);
        h2 = mix2(h2, v.length ^ 0x55aa);
        for (let i = 0; i < v.length; i++) {
          h1 = mix1(h1, v.charCodeAt(i));
          h2 = mix2(h2, v.charCodeAt(i) ^ 0xa5);
        }
      } else if (typeof v === 'boolean') {
        h1 = mix1(h1, v ? 0x1111 : 0x2222);
        h2 = mix2(h2, v ? 0x3333 : 0x4444);
      } else if (v === null) {
        h1 = mix1(h1, 0xaaaa);
        h2 = mix2(h2, 0xbbbb);
      } else if (v === undefined) {
        h1 = mix1(h1, 0xcccc);
        h2 = mix2(h2, 0xdddd);
      } else if (Array.isArray(v)) {
        h1 = mix1(h1, v.length | 0x800000);
        h2 = mix2(h2, v.length ^ 0x1234);
        for (const x of v) feed(x);
      } else if (typeof v === 'object') {
        h1 = mix1(h1, 0x5eed);
        h2 = mix2(h2, 0xcafe);
        const keys = Object.keys(v).sort();
        for (const k of keys) {
          feed(k);
          feed(v[k]);
        }
      }
    };
    for (const p of parts) feed(p);
    // Combine both 32-bit lanes into an exact < 2^53 integer:
    // h1 * 2^21 (h1 < 2^32 => product < 2^53) + top 21 bits of h2.
    return h1 * 0x200000 + (h2 >>> 11);
  }
  
  /**
   * Batch process multiple string-to-ID conversions
   * @param {Array<string>} strings - Array of strings to convert
   * @returns {Array<number>} Array of IDs
   */
  batchGetStringIds(strings) {
    const results = new Array(strings.length);
    
    for (let i = 0; i < strings.length; i++) {
      results[i] = this.getStringId(strings[i]);
    }
    
    return results;
  }

  getRelationIdSnapshot() {
    const entries = [];
    for (const [relation, id] of this.relationIdCache.entries()) {
      entries.push([relation, id]);
    }
    return {
      entries,
      nextRelationId: this.nextRelationId
    };
  }

  loadRelationIdSnapshot(snapshot) {
    if (!snapshot || !Array.isArray(snapshot.entries)) return;
    this.relationIdCache.clear();
    let maxId = 0;
    for (const [relation, id] of snapshot.entries) {
      this.relationIdCache.set(relation, id);
      if (id > maxId) maxId = id;
    }
    this.nextRelationId = snapshot.nextRelationId || (maxId + 1);
  }
  
  /**
   * Batch process multiple composite key creations
   * @param {Array<Object>} relations - Array of {srcId, relation, dstId} objects
   * @returns {Array<number>} Array of composite keys
   */
  batchCreateCompositeKeys(relations) {
    const results = new Array(relations.length);
    
    for (let i = 0; i < relations.length; i++) {
      const { srcId, relation, dstId } = relations[i];
      results[i] = this.createCompositeKey(srcId, relation, dstId);
    }
    
    return results;
  }
  
  /**
   * Get statistics (minimal overhead)
   * @returns {Object} Statistics
   */
  getStats() {
    return {
      totalStrings: this.stringToId.size,
      totalRelations: this.relationIdCache.size,
      nextId: this.nextId,
      nextRelationId: this.nextRelationId,
      maxSrcId: this.options.maxSrcId,
      maxRelationId: this.options.maxRelationId,
      maxDstId: this.options.maxDstId
    };
  }
  
  /**
   * Clear all mappings and reset state
   */
  clear() {
    this.stringToId.clear();
    this.idToString.clear();
    this.relationIdCache.clear();
    this.nextId = 1;
    this.nextRelationId = 1;
  }
}

/**
 * UnifiedGraphIndices - Enhanced GraphIndices using UnifiedKeyManager
 */
export class UnifiedGraphIndices {
  constructor(options = {}) {
    this.keyManager = new UnifiedKeyManager(options);
    
    // Use native Maps for best performance (as proven by benchmarks)
    this.relationsBySrcRelDst = new Map();
    this.relationsBySrcRel = new Map();
    this.relationsByDstRel = new Map();
    this.relationsByRel = new Map();
    this.outgoingEdges = new Map();
    this.incomingEdges = new Map();
  }
  
  /**
   * Add relation with optimized key generation
   * @param {Object} relationObj - Relation object
   */
  addRelation(relationObj) {
    const { src: srcId, rel: relation, dst: dstId } = relationObj;
    
    // Create optimized composite keys
    const directKey = this.keyManager.createCompositeKey(srcId, relation, dstId);
    const srcRelKey = this.keyManager.createSrcRelKey(srcId, relation);
    const dstRelKey = this.keyManager.createDstRelKey(dstId, relation);
    
    // Store in indices
    this.relationsBySrcRelDst.set(directKey, relationObj);
    
    // Source-relation index
    if (!this.relationsBySrcRel.has(srcRelKey)) {
      this.relationsBySrcRel.set(srcRelKey, new Set());
    }
    this.relationsBySrcRel.get(srcRelKey).add(relationObj);
    
    // Destination-relation index
    if (!this.relationsByDstRel.has(dstRelKey)) {
      this.relationsByDstRel.set(dstRelKey, new Set());
    }
    this.relationsByDstRel.get(dstRelKey).add(relationObj);
    
    // Relation name index
    if (!this.relationsByRel.has(relation)) {
      this.relationsByRel.set(relation, new Set());
    }
    this.relationsByRel.get(relation).add(relationObj);
    
    // Update adjacency lists
    if (!this.outgoingEdges.has(srcId)) {
      this.outgoingEdges.set(srcId, new Set());
    }
    this.outgoingEdges.get(srcId).add(dstId);
    
    if (!this.incomingEdges.has(dstId)) {
      this.incomingEdges.set(dstId, new Set());
    }
    this.incomingEdges.get(dstId).add(srcId);
  }
  
  /**
   * Get direct relation with optimized lookup
   * @param {number} srcId - Source ID
   * @param {string} relation - Relation name
   * @param {number} dstId - Destination ID
   * @returns {Object|undefined} Relation object or undefined
   */
  getDirectRelation(srcId, relation, dstId) {
    const directKey = this.keyManager.createCompositeKey(srcId, relation, dstId);
    return this.relationsBySrcRelDst.get(directKey);
  }
  
  /**
   * Get relations from source with optimized lookup
   * @param {number} srcId - Source ID
   * @param {string} relation - Relation name
   * @returns {Array} Array of relation objects
   */
  getRelationsFromSrc(srcId, relation) {
    const srcRelKey = this.keyManager.createSrcRelKey(srcId, relation);
    const relations = this.relationsBySrcRel.get(srcRelKey);
    return relations ? Array.from(relations) : [];
  }
  
  /**
   * Get relations to destination with optimized lookup
   * @param {number} dstId - Destination ID
   * @param {string} relation - Relation name
   * @returns {Array} Array of relation objects
   */
  getRelationsToDst(dstId, relation) {
    const dstRelKey = this.keyManager.createDstRelKey(dstId, relation);
    const relations = this.relationsByDstRel.get(dstRelKey);
    return relations ? Array.from(relations) : [];
  }
  
  /**
   * Get relations by name
   * @param {string} relation - Relation name
   * @returns {Array} Array of relation objects
   */
  getRelationsByName(relation) {
    const relations = this.relationsByRel.get(relation);
    return relations ? Array.from(relations) : [];
  }
  
  /**
   * Build indices from relations array
   * @param {Array} relations - Array of relation objects
   */
  buildIndices(relations) {
    this.clear();
    
    for (const relationObj of relations) {
      this.addRelation(relationObj);
    }
  }
  
  /**
   * Clear all indices
   */
  clear() {
    this.relationsBySrcRelDst.clear();
    this.relationsBySrcRel.clear();
    this.relationsByDstRel.clear();
    this.relationsByRel.clear();
    this.outgoingEdges.clear();
    this.incomingEdges.clear();
    this.keyManager.clear();
  }
  
  /**
   * Get statistics
   * @returns {Object} Statistics
   */
  getStats() {
    return {
      keyManager: this.keyManager.getStats(),
      indices: {
        directRelations: this.relationsBySrcRelDst.size,
        srcRelRelations: this.relationsBySrcRel.size,
        dstRelRelations: this.relationsByDstRel.size,
        namedRelations: this.relationsByRel.size,
        outgoingEdges: this.outgoingEdges.size,
        incomingEdges: this.incomingEdges.size
      }
    };
  }
}
