// Helper function for cosine distance
function cosineDistance(vec1, vec2) {
  if (!vec1 || !vec2 || vec1.length !== vec2.length) return Infinity;
  
  let dotProduct = 0;
  let norm1 = 0;
  let norm2 = 0;
  
  for (let i = 0; i < vec1.length; i++) {
    dotProduct += vec1[i] * vec2[i];
    norm1 += vec1[i] * vec1[i];
    norm2 += vec2[i] * vec2[i];
  }
  
  const magnitude = Math.sqrt(norm1) * Math.sqrt(norm2);
  if (magnitude === 0) return Infinity;
  
  return 1 - (dotProduct / magnitude);
}

export class GraphTraversal {
  constructor(arbiter) {
    this.arbiter = arbiter;
    // OPTIMIZED: Cache adjacency lists to avoid O(R) scans
    this.adjacencyCache = new Map();
    this.adjacencyCacheValid = false;
  }

  // Ensure adjacency cache is built and valid
  _ensureAdjacencyCache() {
    if (this.adjacencyCacheValid) return;
    
    this.adjacencyCache.clear();
    // Build once, use many times - O(R) cost amortized
    for (const {src, dst} of this.arbiter.relations) {
      if (!this.adjacencyCache.has(src)) this.adjacencyCache.set(src, new Set());
      if (!this.adjacencyCache.has(dst)) this.adjacencyCache.set(dst, new Set());
      this.adjacencyCache.get(src).add(dst);
      this.adjacencyCache.get(dst).add(src); // undirected for now
    }
    this.adjacencyCacheValid = true;
  }

  // Invalidate cache when relations change
  invalidateAdjacencyCache() {
    this.adjacencyCacheValid = false;
  }

  // Fast landmark-based graph distance estimate (not true shortest path)
  estimateGraphDistance(key1, key2) {
    const id1 = this.arbiter.nodeIdByKey.get(key1);
    const id2 = this.arbiter.nodeIdByKey.get(key2);
    if (id1 === undefined || id2 === undefined) return Infinity;
    if (id1 === id2) return 0;
    
    // Use only stored hubs if any, else fall back to a random sample of nodes
    let hubIds;
    if (this.arbiter.hubIds.size > 0) {
      hubIds = Array.from(this.arbiter.hubIds);
    } else {
      // Randomly sample up to maxLandmarkSample nodes as fallback hubs
      const allIds = Array.from(this.arbiter.nodes.keys());
      if (allIds.length <= this.arbiter.maxLandmarkSample) {
        hubIds = allIds;
      } else {
        // Fisher-Yates shuffle for unbiased sampling
        for (let i = allIds.length - 1; i > 0; i--) {
          const j = Math.floor(Math.random() * (i + 1));
          [allIds[i], allIds[j]] = [allIds[j], allIds[i]];
        }
        hubIds = allIds.slice(0, this.arbiter.maxLandmarkSample);
      }
    }
    
    if (hubIds.length === 0) return Infinity;
    
    let minDist = Infinity;
    for (const hubId of hubIds) {
      const v1 = this.arbiter.nodes.get(id1)?.embedding;
      const v2 = this.arbiter.nodes.get(id2)?.embedding;
      const hub = this.arbiter.nodes.get(hubId)?.embedding;
      if (!v1 || !v2 || !hub) continue;
      
      try {
        const d1 = cosineDistance(v1, hub);
        const d2 = cosineDistance(hub, v2);
        if (d1 !== Infinity && d2 !== Infinity) {
          minDist = Math.min(minDist, d1 + d2);
        }
      } catch (e) {
        // skip this hub
      }
    }
    return minDist;
  }

  // True shortest path (in hops) using BFS over relations (ignores relation types)
  shortestPathLength(key1, key2) {
    const id1 = this.arbiter.nodeIdByKey.get(key1);
    const id2 = this.arbiter.nodeIdByKey.get(key2);
    if (id1 === undefined || id2 === undefined) return Infinity;
    if (id1 === id2) return 0;
    
    // OPTIMIZED: Use cached adjacency instead of rebuilding every time
    this._ensureAdjacencyCache();
    
    // BFS
    const visited = new Set();
    const queue = [[id1, 0]];
    visited.add(id1);
    
    while (queue.length > 0) {
      const [curr, dist] = queue.shift();
      if (curr === id2) return dist;
      
      for (const neighbor of (this.adjacencyCache.get(curr) || [])) {
        if (!visited.has(neighbor)) {
          visited.add(neighbor);
          queue.push([neighbor, dist + 1]);
        }
      }
    }
    return Infinity;
  }

  // Flexible random walk with per-step and weighted neighbor logic
  // walk(startKey, steps, stepFn, neighborWeightsFn)
  // - stepFn(currentKey, stepIndex, pathSoFar): called at each step
  // - neighborWeightsFn(currentKey, neighbors): returns array of weights for neighbors
  walk(startKey, steps, stepFn = () => {}, neighborWeightsFn = null) {
    const id = this.arbiter.nodeIdByKey.get(startKey);
    if (id === undefined) return [];
    
    // OPTIMIZED: Use cached adjacency instead of rebuilding every time
    this._ensureAdjacencyCache();
    
    let curr = id;
    const path = [curr];
    
    for (let i = 0; i < steps; i++) {
      const neighbors = Array.from(this.adjacencyCache.get(curr) || []);
      if (neighbors.length === 0) break;
      
      // Get weights for neighbors
      let weights;
      if (neighborWeightsFn) {
        // Convert node IDs to keys for neighborWeightsFn
        const idToKey = new Map(Array.from(this.arbiter.nodeIdByKey.entries()).map(([k, v]) => [v, k]));
        weights = neighborWeightsFn(idToKey.get(curr), neighbors.map(nid => idToKey.get(nid)));
      } else {
        weights = Array(neighbors.length).fill(1); // uniform
      }
      
      // Weighted random choice
      const total = weights.reduce((a, b) => a + b, 0);
      if (total <= 0) break;
      
      let r = Math.random() * total;
      let nextIdx = 0;
      for (; nextIdx < weights.length; nextIdx++) {
        r -= weights[nextIdx];
        if (r < 0) break;
      }
      
      curr = neighbors[nextIdx];
      path.push(curr);
      
      // Call stepFn with key, step index, and path so far (as keys)
      const idToKey = new Map(Array.from(this.arbiter.nodeIdByKey.entries()).map(([k, v]) => [v, k]));
      stepFn(idToKey.get(curr), i + 1, path.map(nid => idToKey.get(nid)));
    }
    
    // Convert node IDs back to keys
    const idToKey = new Map(Array.from(this.arbiter.nodeIdByKey.entries()).map(([k, v]) => [v, k]));
    return path.map(nid => idToKey.get(nid));
  }

  // Simple Monte Carlo random walker for the current relation graph
  monteCarloWalk(startKey, steps, options = {}) {
    // Uniform weights, no-op stepFn
    return this.walk(startKey, steps, () => {}, null);
  }

  // Helper: Find all paths between two nodes via a relation with reliability decay
  findPaths(startId, endId, relation, maxDepth, minReliability, decayFactor, visited, currentPath = [], currentPoss = 1.0, currentRel = 1.0, reverse = false) {
    if (startId === endId) {
      const path = {
        nodes: [...currentPath, this.arbiter.keyByNodeId.get(endId)],
        hops: currentPath.length,
        possibility: currentPoss,
        reliability: currentRel
      };
      return [path];
    }
    
    if (maxDepth <= 0 || currentRel < minReliability || visited.has(startId)) {
      return [];
    }
    
    visited.add(startId);
    const paths = [];
    
    // Find edges with the specified relation (direction depends on reverse flag)
    const edges = this.arbiter.relations.filter(r => {
      if (reverse) {
        return r.dst === startId && r.rel === relation;
      } else {
        return r.src === startId && r.rel === relation;
      }
    });
    
    for (const edge of edges) {
      const nextId = reverse ? edge.src : edge.dst;
      const nextKey = this.arbiter.keyByNodeId.get(nextId);
      if (!nextKey) continue;
      
      // Calculate decayed reliability and combined possibility
      const nextRel = currentRel * edge.reliability * decayFactor;
      const nextPoss = Math.min(currentPoss, edge.possibility); // Min for conjunction along path
      
      // Recursively find paths from next node
      const subPaths = this.findPaths(
        nextId,
        endId,
        relation,
        maxDepth - 1,
        minReliability,
        decayFactor,
        new Set(visited),
        [...currentPath, this.arbiter.keyByNodeId.get(startId)],
        nextPoss,
        nextRel,
        reverse
      );
      
      paths.push(...subPaths);
    }
    
    visited.delete(startId);
    return paths;
  }

  // Get a tree-structured situation JSON for a node, recursively, up to maxDepth
  getSituationTree(nodeId, maxDepth = 2, visited = new Set()) {
    const node = this.arbiter.nodes.get(nodeId);
    if (!node) return null;
    if (visited.has(nodeId) || maxDepth < 0) return undefined; // avoid cycles
    visited.add(nodeId);
    
    // Gather outgoing relations (edges out only, for tree structure)
    const relsByName = {};
    for (const rel of this.arbiter.relations) {
      if (rel.src === nodeId) {
        const relName = rel.rel;
        const targetId = rel.dst;
        const targetNode = this.arbiter.nodes.get(targetId);
        if (!targetNode) continue;
        
        if (!relsByName[relName]) {
          relsByName[relName] = [];
        }
        
        // Recursively build the subtree for the target node
        const subtree = this.getSituationTree(targetId, maxDepth - 1, new Set(visited));
        relsByName[relName].push({
          [targetNode.key]: {
            type: targetNode.type,
            ...(subtree && subtree[targetNode.key] ? { relations: subtree[targetNode.key].relations } : {})
          }
        });
      }
    }
    
    // Build the result for this node
    const result = {
      [node.key]: {
        type: node.type,
        relations: relsByName
      }
    };
    return result;
  }
} 
