/**
 * 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);
    return `${srcId}|${relationId}|${dstId}`;
  }
  
  /**
   * Create source-relation key
   * @param {number} srcId - Source ID
   * @param {string} relation - Relation name
   * @returns {string} Composite 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: fromSrc and toDst lookups share the relation lookup
    // cache, and identical `id|relId` keys made the second-direction lookup
    // return the first direction's cached edges.
    return `${srcId}|${relationId}|s`;
  }
  
  /**
   * Create destination-relation key
   * @param {number} dstId - Destination ID
   * @param {string} relation - Relation name
   * @returns {number} Composite 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 `${dstId}|${relationId}|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} 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}`);
    }

    return JSON.stringify([userId, objectId, steps]);
  }
  
  /**
   * 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
      }
    };
  }
}
