// DNScope-reduced/static/js/graph.js
/**
 * Graph visualization module for DNScope
 * Handles network graph rendering using vis.js with proper large entity node hiding
 * UPDATED: Added time-based blue gradient edge coloring system
 */
const contextMenuCSS = `
.graph-context-menu {
    position: fixed;
    z-index: 1000;
    background: linear-gradient(135deg, #2a2a2a 0%, #1e1e1e 100%);
    border: 1px solid #444;
    border-radius: 6px;
    box-shadow: 0 8px 25px rgba(0,0,0,0.6);
    display: none;
    font-family: 'Roboto Mono', monospace;
    font-size: 0.9rem;
    color: #c7c7c7;
    min-width: 180px;
    overflow: hidden;
}
.graph-context-menu ul {
    list-style: none;
    padding: 0.5rem 0;
    margin: 0;
}
.graph-context-menu ul li {
    padding: 0.75rem 1rem;
    cursor: pointer;
    transition: all 0.2s ease;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.graph-context-menu ul li:hover {
    background: linear-gradient(135deg, #3a3a3a 0%, #2e2e2e 100%);
    color: #00ff41;
}
.graph-context-menu .menu-icon {
    font-size: 0.9rem;
    width: 1.2rem;
    text-align: center;
}
.graph-context-menu ul li:first-child {
    border-top: none;
}
.graph-context-menu ul li:last-child {
    border-bottom: none;
}
.time-control-container {
    margin-bottom: 0.5rem;
    padding: 0.5rem;
    background: rgba(42, 42, 42, 0.3);
    border-radius: 4px;
    border: 1px solid #444;
}
.time-control-label {
    font-size: 0.8rem;
    color: #c7c7c7;
    margin-bottom: 0.3rem;
    display: block;
}
.time-control-input {
    width: 100%;
    padding: 0.3rem;
    background: #1a1a1a;
    border: 1px solid #555;
    border-radius: 3px;
    color: #c7c7c7;
    font-family: 'Roboto Mono', monospace;
    font-size: 0.75rem;
}
.time-control-input:focus {
    outline: none;
    border-color: #00ff41;
}
.time-gradient-info {
    font-size: 0.7rem;
    color: #999;
    margin-top: 0.3rem;
    text-align: center;
}
`;
class GraphManager {
    constructor(containerId) {
        this.container = document.getElementById(containerId);
        this.network = null;
        this.nodes = new vis.DataSet();
        this.edges = new vis.DataSet();
        this.isInitialized = false;
        this.currentLayout = 'physics';
        this.nodeInfoPopup = null;
        this.contextMenu = null;
        this.history = [];
        this.filterPanel = null;
        this.initialTargetIds = new Set();
        // Track large entity members for proper hiding
        this.largeEntityMembers = new Set();
        this.isScanning = false;
        
        // Manual refresh button for polling optimization
        this.manualRefreshButton = null;
        this.manualRefreshHandler = null; // Store the handler
        // Time-based gradient settings
        this.timeOfInterest = new Date(); // Default to now
        this.edgeTimestamps = new Map(); // Store edge ID -> timestamp mapping
        
        // Gradient colors: grey-ish dark to retina-melting light blue
        this.gradientColors = {
            dark: '#6b7280',     // Grey-ish dark
            light: '#00bfff'     // Retina-melting light blue
        };
        this.options = {
            nodes: {
                shape: 'dot',
                size: 15,
                font: {
                    size: 12,
                    color: '#c7c7c7',
                    face: 'Roboto Mono, monospace',
                    background: 'rgba(26, 26, 26, 0.9)',
                    strokeWidth: 2,
                    strokeColor: '#000000'
                },
                borderWidth: 2,
                borderColor: '#444',
                scaling: {
                    min: 10,
                    max: 30,
                    label: {
                        enabled: true,
                        min: 8,
                        max: 16
                    }
                },
                chosen: {
                    node: (values, id, selected, hovering) => {
                        values.borderColor = '#00ff41';
                        values.borderWidth = 3;
                    }
                }
            },
            edges: {
                width: 2,
                color: {
                    color: '#555',
                    highlight: '#00ff41',
                    hover: '#ff9900',
                    inherit: false
                },
                font: {
                    size: 10,
                    color: '#999',
                    face: 'Roboto Mono, monospace',
                    background: 'rgba(26, 26, 26, 0.8)',
                    strokeWidth: 1,
                    strokeColor: '#000000'
                },
                arrows: {
                    to: {
                        enabled: true,
                        scaleFactor: 1,
                        type: 'arrow'
                    }
                },
                smooth: {
                    enabled: true,
                    type: 'dynamic',
                    roundness: 0.6
                },
                chosen: {
                    edge: (values, id, selected, hovering) => {
                        values.color = '#00ff41';
                        values.width = 4;
                    }
                }
            },
            physics: {
                enabled: true,
                stabilization: {
                    enabled: true,
                    iterations: 150,
                    updateInterval: 50
                },
                barnesHut: {
                    gravitationalConstant: -3000,
                    centralGravity: 0.4,
                    springLength: 120,
                    springConstant: 0.05,
                    damping: 0.1,
                    avoidOverlap: 0.2
                },
                maxVelocity: 30,
                minVelocity: 0.1,
                solver: 'barnesHut',
                timestep: 0.4,
                adaptiveTimestep: true
            },
            interaction: {
                hover: true,
                hoverConnectedEdges: true,
                selectConnectedEdges: true,
                tooltipDelay: 300,
                hideEdgesOnDrag: false,
                hideNodesOnDrag: false,
                zoomView: true,
                dragView: true,
                multiselect: true
            },
            layout: {
                improvedLayout: true,
                randomSeed: 2
            }
        };
    if (typeof document !== 'undefined') {
        const style = document.createElement('style');
        style.textContent = contextMenuCSS;
        document.head.appendChild(style);
    }
        this.createNodeInfoPopup();
        this.createContextMenu();
        document.body.addEventListener('click', () => this.hideContextMenu());
    }
    /**
     * Create floating node info popup
     */
    createNodeInfoPopup() {
        this.nodeInfoPopup = document.createElement('div');
        this.nodeInfoPopup.className = 'node-info-popup';
        this.nodeInfoPopup.style.display = 'none';
        document.body.appendChild(this.nodeInfoPopup);
    }
    
    /**
     * Create context menu
     */
    createContextMenu() {
        // Remove existing context menu if it exists
        const existing = document.getElementById('graph-context-menu');
        if (existing) {
            existing.remove();
        }
        
        this.contextMenu = document.createElement('div');
        this.contextMenu.id = 'graph-context-menu';
        this.contextMenu.className = 'graph-context-menu';
        this.contextMenu.style.display = 'none';
        
        // Prevent body click listener from firing when clicking the menu itself
        this.contextMenu.addEventListener('click', (event) => {
            event.stopPropagation();
        });
        document.body.appendChild(this.contextMenu);
    }
    /**
     * Initialize the network graph
     */
    initialize() {
        if (this.isInitialized) {
            return;
        }
        try {
            const data = {
                nodes: this.nodes,
                edges: this.edges
            };
            this.network = new vis.Network(this.container, data, this.options);
            this.setupNetworkEvents();
            this.isInitialized = true;
            // Hide placeholder
            const placeholder = this.container.querySelector('.graph-placeholder');
            if (placeholder) {
                placeholder.style.display = 'none';
            }
            // Add graph controls
            this.addGraphControls();
            this.addFilterPanel();
            console.log('Graph initialized successfully');
        } catch (error) {
            console.error('Failed to initialize graph:', error);
            this.showError('Failed to initialize visualization');
        }
    }
    /**
     * Add interactive graph controls with time of interest control
     * UPDATED: Added time-based edge coloring controls
     */
    addGraphControls() {
        const controlsContainer = document.createElement('div');
        controlsContainer.className = 'graph-controls';
        
        // Format current date/time for the input
        const currentDateTime = this.formatDateTimeForInput(this.timeOfInterest);
        
        controlsContainer.innerHTML = `
            
            [FIT] 
            [PHYSICS] 
            [CLUSTER] 
            [UNHIDE] 
            [REVERT] 
            [REFRESH] 
        `;
        this.container.appendChild(controlsContainer);
        // Add control event listeners
        document.getElementById('graph-fit').addEventListener('click', () => this.fitView());
        document.getElementById('graph-physics').addEventListener('click', () => this.togglePhysics());
        document.getElementById('graph-cluster').addEventListener('click', () => this.toggleClustering());
        document.getElementById('graph-unhide').addEventListener('click', () => this.unhideAll());
        document.getElementById('graph-revert').addEventListener('click', () => this.revertLastAction());
        
        // Time of interest control
        document.getElementById('time-of-interest').addEventListener('change', (e) => {
            this.timeOfInterest = new Date(e.target.value);
            console.log('Time of interest updated:', this.timeOfInterest);
            this.updateEdgeColors();
        });
        
        // Manual refresh button - handler will be set by main app
        this.manualRefreshButton = document.getElementById('graph-manual-refresh');
        // If a handler was set before the button existed, attach it now
        if (this.manualRefreshButton && this.manualRefreshHandler) {
            this.manualRefreshButton.addEventListener('click', this.manualRefreshHandler);
        }
    }
    /**
     * Format date for datetime-local input
     */
    formatDateTimeForInput(date) {
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        const hours = String(date.getHours()).padStart(2, '0');
        const minutes = String(date.getMinutes()).padStart(2, '0');
        return `${year}-${month}-${day}T${hours}:${minutes}`;
    }
    /**
     * Extract relevant timestamp from edge raw_data based on provider
     */
    extractEdgeTimestamp(edge) {
        const rawData = edge.raw_data || {};
        const provider = edge.source_provider || '';
        
        // Check for standardized relevance_timestamp first
        if (rawData.relevance_timestamp) {
            return new Date(rawData.relevance_timestamp);
        }
        
        // Provider-specific timestamp extraction
        switch (provider.toLowerCase()) {
            case 'shodan':
                // Use last_seen timestamp for Shodan
                if (rawData.last_seen) {
                    return new Date(rawData.last_seen);
                }
                break;
                
            case 'crtsh':
                // Use certificate issue date (not_before) for certificates
                if (rawData.cert_not_before) {
                    return new Date(rawData.cert_not_before);
                }
                break;
                
            case 'dns':
            case 'correlation':
            default:
                // Use discovery timestamp for DNS and correlation
                if (edge.discovery_timestamp) {
                    return new Date(edge.discovery_timestamp);
                }
                break;
        }
        
        // Fallback to discovery timestamp or current time
        if (edge.discovery_timestamp) {
            return new Date(edge.discovery_timestamp);
        }
        
        return new Date(); // Default to now if no timestamp available
    }
    /**
     * Calculate time-based blue gradient color
     */
    calculateTimeGradientColor(timestamp) {
        if (!timestamp || !this.timeOfInterest) {
            return this.gradientColors.dark; // Default to dark grey
        }
        
        // Calculate time difference in milliseconds
        const timeDiff = Math.abs(timestamp.getTime() - this.timeOfInterest.getTime());
        
        // Find maximum time difference across all edges for normalization
        let maxTimeDiff = 0;
        this.edgeTimestamps.forEach((edgeTimestamp) => {
            const diff = Math.abs(edgeTimestamp.getTime() - this.timeOfInterest.getTime());
            if (diff > maxTimeDiff) {
                maxTimeDiff = diff;
            }
        });
        
        if (maxTimeDiff === 0) {
            return this.gradientColors.light; // All timestamps are the same
        }
        
        // Calculate gradient position (0 = closest to time of interest, 1 = furthest)
        const gradientPosition = timeDiff / maxTimeDiff;
        
        // Interpolate between light blue (close) and dark grey (far)
        return this.interpolateColor(
            this.gradientColors.light,  // Close to time of interest
            this.gradientColors.dark,   // Far from time of interest
            gradientPosition
        );
    }
    /**
     * Interpolate between two hex colors
     */
    interpolateColor(color1, color2, factor) {
        // Parse hex colors
        const hex1 = color1.replace('#', '');
        const hex2 = color2.replace('#', '');
        
        const r1 = parseInt(hex1.substring(0, 2), 16);
        const g1 = parseInt(hex1.substring(2, 4), 16);
        const b1 = parseInt(hex1.substring(4, 6), 16);
        
        const r2 = parseInt(hex2.substring(0, 2), 16);
        const g2 = parseInt(hex2.substring(2, 4), 16);
        const b2 = parseInt(hex2.substring(4, 6), 16);
        
        // Interpolate
        const r = Math.round(r1 + (r2 - r1) * factor);
        const g = Math.round(g1 + (g2 - g1) * factor);
        const b = Math.round(b1 + (b2 - b1) * factor);
        
        // Convert back to hex
        return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
    }
    /**
     * Update all edge colors based on current time of interest
     */
    updateEdgeColors() {
        const edgeUpdates = [];
        
        this.edges.forEach((edge) => {
            const timestamp = this.edgeTimestamps.get(edge.id);
            const color = this.calculateTimeGradientColor(timestamp);
            
            edgeUpdates.push({
                id: edge.id,
                color: {
                    color: color,
                    highlight: '#00ff41',
                    hover: '#ff9900'
                }
            });
        });
        
        if (edgeUpdates.length > 0) {
            this.edges.update(edgeUpdates);
            console.log(`Updated ${edgeUpdates.length} edge colors based on time gradient`);
        }
    }
    
    /**
     * Set the manual refresh button click handler
     * @param {Function} handler - Function to call when manual refresh is clicked
     */
    setManualRefreshHandler(handler) {
        this.manualRefreshHandler = handler;
        // If the button already exists, attach the handler
        if (this.manualRefreshButton && typeof handler === 'function') {
            this.manualRefreshButton.addEventListener('click', handler);
        }
    }
    
    /**
     * Show or hide the manual refresh button
     * @param {boolean} show - Whether to show the button
     */
    showManualRefreshButton(show) {
        if (this.manualRefreshButton) {
            this.manualRefreshButton.style.display = show ? 'inline-block' : 'none';
        }
    }
    
    addFilterPanel() {
        this.filterPanel = document.createElement('div');
        this.filterPanel.className = 'graph-filter-panel';
        this.container.appendChild(this.filterPanel);
    }
    /**
     * Setup network event handlers
     */
    setupNetworkEvents() {
        if (!this.network) return;
        // FIXED: Right-click context menu
        this.container.addEventListener('contextmenu', (event) => {
            event.preventDefault();
            
            // Get coordinates relative to the canvas
            const pointer = {
                x: event.offsetX,
                y: event.offsetY
            };
            
            const nodeId = this.network.getNodeAt(pointer);
            
            if (nodeId) {
                // Pass the original client event for positioning
                this.showContextMenu(nodeId, event);
            } else {
                this.hideContextMenu();
            }
        });
        // Node click event with details
        this.network.on('click', (params) => {
            this.hideContextMenu();
            if (params.nodes.length > 0) {
                const nodeId = params.nodes[0];
                if (this.network.isCluster(nodeId)) {
                    this.network.openCluster(nodeId);
                } else {
                    const node = this.nodes.get(nodeId);
                    if (node) {
                        this.showNodeDetails(node);
                        this.highlightNodeConnections(nodeId);
                    }
                }
            } else {
                this.clearHighlights();
            }
        });
        // Hover events
        this.network.on('hoverNode', (params) => {
            const nodeId = params.node;
            const node = this.nodes.get(nodeId);
            if (node) {
                this.highlightConnectedNodes(nodeId, true);
            }
        });
        // Stabilization events with progress
        this.network.on('stabilizationProgress', (params) => {
            const progress = params.iterations / params.total;
        });
        this.network.on('stabilizationIterationsDone', () => {
            this.onStabilizationComplete();
        });
        // Click away to hide context menu
        document.addEventListener('click', (e) => {
            if (!this.contextMenu.contains(e.target)) {
                this.hideContextMenu();
            }
        });
    }
    updateGraph(graphData) {
        if (!graphData || !graphData.nodes || !graphData.edges) {
            console.warn('Invalid graph data received');
            return;
        }
        try {
            if (!this.isInitialized) {
                this.initialize();
            }
            this.initialTargetIds = new Set(graphData.initial_targets || []);
            const hasData = graphData.nodes.length > 0 || graphData.edges.length > 0;
            
            const placeholder = this.container.querySelector('.graph-placeholder');
            if (placeholder) {
                placeholder.style.display = hasData ? 'none' : 'flex';
            }
            if (!hasData) {
                this.nodes.clear();
                this.edges.clear();
                this.edgeTimestamps.clear();
                return;
            }
            const nodeMap = new Map(graphData.nodes.map(node => [node.id, node]));
            // FIXED: Process all nodes first, then apply hiding logic correctly
            const processedNodes = graphData.nodes.map(node => {
                const processed = this.processNode(node);
                
                // FIXED: Only hide if node is still a large entity member
                if (node.metadata && node.metadata.large_entity_id) {
                    processed.hidden = true;
                } else {
                    // FIXED: Ensure extracted nodes are visible
                    processed.hidden = false;
                }
                
                return processed;
            });
            
            const processedEdges = graphData.edges.map(edge => {
                let fromNode = nodeMap.get(edge.from);
                let toNode = nodeMap.get(edge.to);
                let fromId = edge.from;
                let toId = edge.to;
                // FIXED: Only re-route if nodes are STILL in large entities
                if (fromNode && fromNode.metadata && fromNode.metadata.large_entity_id) {
                    fromId = fromNode.metadata.large_entity_id;
                }
                if (toNode && toNode.metadata && toNode.metadata.large_entity_id) {
                    toId = toNode.metadata.large_entity_id;
                }
                
                // Avoid self-referencing edges from re-routing
                if (fromId === toId) {
                    return null; 
                }
                const reRoutedEdge = { ...edge, from: fromId, to: toId };
                return this.processEdge(reRoutedEdge);
            }).filter(Boolean); // Remove nulls from self-referencing edges
            const existingNodeIds = this.nodes.getIds();
            const existingEdgeIds = this.edges.getIds();
            const newNodes = processedNodes.filter(node => !existingNodeIds.includes(node.id));
            const newEdges = processedEdges.filter(edge => !existingEdgeIds.includes(edge.id));
            // FIXED: Update all nodes to ensure extracted nodes become visible
            this.nodes.update(processedNodes);
            this.edges.update(processedEdges);
            
            // Update edge timestamps and colors for time-based gradient
            this.updateEdgeTimestampsAndColors(graphData.edges);
            
            this.updateFilterControls();
            this.applyAllFilters();
            if (newNodes.length > 0 || newEdges.length > 0) {
                setTimeout(() => this.highlightNewElements(newNodes, newEdges), 100);
            }
            if (this.nodes.length <= 10 || existingNodeIds.length === 0) {
                setTimeout(() => this.fitView(), 800);
            }
            
        } catch (error) {
            console.error('Failed to update graph:', error);
            this.showError('Failed to update visualization');
        }
    }
    /**
     * Update edge timestamps and apply time-based gradient colors
     */
    updateEdgeTimestampsAndColors(edgeData) {
        // Extract timestamps from raw edge data
        edgeData.forEach(edge => {
            const edgeId = `${edge.from}-${edge.to}-${edge.label}`;
            const timestamp = this.extractEdgeTimestamp(edge);
            this.edgeTimestamps.set(edgeId, timestamp);
        });
        
        // Update edge colors based on new timestamps
        this.updateEdgeColors();
    }
    analyzeCertificateInfo(attributes) {
        let hasCertificates = false;
        let hasValidCertificates = false;
        let hasExpiredCertificates = false;
        
        for (const attr of attributes) {
            const attrName = (attr.name || '').toLowerCase();
            const attrProvider = (attr.provider || '').toLowerCase();
            const attrValue = attr.value;
            
            // Look for certificate attributes from crtsh provider
            if (attrProvider === 'crtsh' || attrName.startsWith('cert_')) {
                hasCertificates = true;
                
                // Check certificate validity using raw attribute names
                if (attrName === 'cert_is_currently_valid') {
                    if (attrValue === true) {
                        hasValidCertificates = true;
                    } else if (attrValue === false) {
                        hasExpiredCertificates = true;
                    }
                }
                // Check for expiry indicators
                else if (attrName === 'cert_expires_soon' && attrValue === true) {
                    hasExpiredCertificates = true;
                }
                else if (attrName.includes('expired') && attrValue === true) {
                    hasExpiredCertificates = true;
                }
            }
        }
        
        return {
            hasCertificates,
            hasValidCertificates,
            hasExpiredCertificates,
            hasExpiredOnly: hasExpiredCertificates && !hasValidCertificates
        };
    }
    /**
     * UPDATED: Helper method to find an attribute by name in the standardized attributes list
     * @param {Array} attributes - List of StandardAttribute objects
     * @param {string} name - Attribute name to find
     * @returns {Object|null} The attribute object if found, null otherwise
     */
    findAttributeByName(attributes, name) {
        if (!Array.isArray(attributes)) {
            return null;
        }
        return attributes.find(attr => attr.name === name) || null;
    }
    /**
     * UPDATED: Process node data with styling and metadata for the flat data model
     * @param {Object} node - Raw node data with standardized attributes
     * @returns {Object} Processed node data
     */
    processNode(node) {
        const processedNode = {
            id: node.id,
            label: this.formatNodeLabel(node.id, node.type),
            color: this.getNodeColor(node.type),
            size: this.getNodeSize(node.type),
            borderColor: this.getNodeBorderColor(node.type),
            shape: this.getNodeShape(node.type),
            attributes: node.attributes || [],
            description: node.description || '',
            metadata: node.metadata || {},
            type: node.type,
            incoming_edges: node.incoming_edges || [],
            outgoing_edges: node.outgoing_edges || []
        };
        if (node.max_depth_reached) {
            processedNode.borderColor = '#ff0000'; // Red border for max depth
        }
        
        // FIXED: Certificate-based domain coloring
        if (node.type === 'domain' && Array.isArray(node.attributes)) {
            const certInfo = this.analyzeCertificateInfo(node.attributes);
            
            if (certInfo.hasExpiredOnly) {
                // Red for domains with only expired/invalid certificates
                processedNode.color = '#ff6b6b';
                processedNode.borderColor = '#cc5555';
            } else if (!certInfo.hasCertificates) {
                // Grey for domains with no certificates
                processedNode.color = '#c7c7c7';
                processedNode.borderColor = '#999999';
            }
            // Green for valid certificates (default color)
        }
        
        // Handle merged correlation objects
        if (node.type === 'correlation_object') {
            const correlationValueAttr = this.findAttributeByName(node.attributes, 'correlation_value');
            const value = correlationValueAttr ? correlationValueAttr.value : 'Unknown';
            const displayValue = typeof value === 'string' && value.length > 20 ? value.substring(0, 17) + '...' : value;
            processedNode.label = `${displayValue}`;
            processedNode.title = `Correlation: ${value}`;
        }
        return processedNode;
    }
    /**
     * Process edge data with styling, metadata, and time-based gradient colors
     * @param {Object} edge - Raw edge data
     * @returns {Object} Processed edge data
     */
    processEdge(edge) {
        const edgeId = `${edge.from}-${edge.to}-${edge.label}`;
        
        // Extract timestamp for this edge
        const timestamp = this.extractEdgeTimestamp(edge);
        this.edgeTimestamps.set(edgeId, timestamp);
        
        // Calculate time-based gradient color
        const timeGradientColor = this.calculateTimeGradientColor(timestamp);
        
        const processedEdge = {
            id: edgeId,
            from: edge.from,
            to: edge.to,
            label: edge.label, // Correctly access the label directly
            title: this.createEdgeTooltip(edge),
            color: {
                color: timeGradientColor,
                highlight: '#00ff41',
                hover: '#ff9900'
            },
            metadata: {
                relationship_type: edge.label,
                source_provider: edge.source_provider,
                discovery_timestamp: edge.discovery_timestamp
            }
        };
        return processedEdge;
    }
    /**
     * Format node label for display
     * @param {string} nodeId - Node identifier
     * @param {string} nodeType - Node type
     * @returns {string} Formatted label
     */
    formatNodeLabel(nodeId, nodeType) {
        if (typeof nodeId !== 'string') return '';
        if (nodeId.length > 20) {
            return nodeId.substring(0, 17) + '...';
        }
        return nodeId;
    }
    
    /**
     * Get node color based on type
     * @param {string} nodeType - Node type
     * @returns {string} Color value
     */
    getNodeColor(nodeType) {
        const colors = {
            'domain': '#00ff41',     // Green
            'ip': '#ff9900',         // Amber
            'isp': '#00aaff',         // Blue
            'ca': '#ff6b6b', // Red
            'large_entity': '#ff6b6b', // Red for large entities
            'correlation_object': '#9620c0ff'
        };
        return colors[nodeType] || '#ffffff';
    }
    /**
     * Get node border color based on type
     * @param {string} nodeType - Node type
     * @returns {string} Border color value
     */
    getNodeBorderColor(nodeType) {
        const borderColors = {
            'domain': '#00aa2e',
            'ip': '#cc7700',
            'isp': '#0088cc',
            'ca': '#cc5555',
            'correlation_object': '#c235c9ff'
        };
        return borderColors[nodeType] || '#666666';
    }
    /**
     * Get node size based on type
     * @param {string} nodeType - Node type
     * @returns {number} Node size
     */
    getNodeSize(nodeType) {
        const sizes = {
            'domain': 12,
            'ip': 14,
            'isp': 16,
            'ca': 16,
            'correlation_object': 8,
            'large_entity': 25
        };
        return sizes[nodeType] || 12;
    }
    
    /**
     * Get node shape based on type
     * @param {string} nodeType - Node type
     * @returns {string} Shape name
     */
    getNodeShape(nodeType) {
        const shapes = {
            'domain': 'dot',
            'ip': 'square',
            'isp': 'triangle',
            'ca': 'diamond',
            'correlation_object': 'hexagon',
            'large_entity': 'dot'
        };
        return shapes[nodeType] || 'dot';
    }
    
    /**
     * Create edge tooltip with correct provider information and timestamp
     * @param {Object} edge - Edge data
     * @returns {string} HTML tooltip content
     */
    createEdgeTooltip(edge) {
        let tooltip = ``;
        tooltip += `
${edge.label || 'Relationship'}
`;
        
        if (edge.source_provider) {
            tooltip += `
Provider: ${edge.source_provider}
`;
        }
        
        if (edge.discovery_timestamp) {
            const date = new Date(edge.discovery_timestamp);
            tooltip += `
Discovered: ${date.toLocaleString()}
`;
        }
        
        // Add timestamp information for time-based coloring
        const edgeId = `${edge.from}-${edge.to}-${edge.label}`;
        const timestamp = this.edgeTimestamps.get(edgeId);
        if (timestamp) {
            tooltip += `
Data from: ${timestamp.toLocaleString()}
`;
        }
        
        tooltip += `
 `;
        return tooltip;
    }
    /**
     * Determine if node is important based on connections or metadata
     * @param {Object} node - Node data
     * @returns {boolean} True if node is important
     */
    isImportantNode(node) {
        // Mark nodes as important based on criteria
        if (node.type === 'domain' && node.id.includes('www.')) return false;
        if (node.metadata && node.metadata.connection_count > 3) return true;
        if (node.type === 'asn') return true;
        return false;
    }
    /**
     * Show node details in modal
     * @param {Object} node - Node object
     */
    showNodeDetails(node) {
        // Trigger custom event for main application to handle
        const event = new CustomEvent('nodeSelected', {
            detail: { node }
        });
        document.dispatchEvent(event);
    }
    
    /**
     * Hide node info popup
     */
    hideNodeInfoPopup() {
        if (this.nodeInfoPopup) {
            this.nodeInfoPopup.style.display = 'none';
        }
    }
    
    /**
     * Highlight node connections
     * @param {string} nodeId - Node to highlight
     */
    highlightNodeConnections(nodeId) {
        const connectedNodes = this.network.getConnectedNodes(nodeId);
        const connectedEdges = this.network.getConnectedEdges(nodeId);
        
        // Update node colors
        const nodeUpdates = connectedNodes.map(id => ({
            id: id,
            borderColor: '#ff9900',
            borderWidth: 3
        }));
        
        nodeUpdates.push({
            id: nodeId,
            borderColor: '#00ff41',
            borderWidth: 4
        });
        
        // Update edge colors
        const edgeUpdates = connectedEdges.map(id => ({
            id: id,
            color: { color: '#ff9900' },
            width: 3
        }));
        
        this.nodes.update(nodeUpdates);
        this.edges.update(edgeUpdates);
        
        // Store for cleanup
        this.highlightedElements = {
            nodes: connectedNodes.concat([nodeId]),
            edges: connectedEdges
        };
    }
    /**
     * Highlight connected nodes on hover
     * @param {string} nodeId - Node ID
     * @param {boolean} highlight - Whether to highlight or unhighlight
     */
    highlightConnectedNodes(nodeId, highlight) {
        const connectedNodes = this.network.getConnectedNodes(nodeId);
        const connectedEdges = this.network.getConnectedEdges(nodeId);
        
        if (highlight) {
            // Dim all other elements
            this.dimUnconnectedElements([nodeId, ...connectedNodes], connectedEdges);
        }
    }
    /**
     * Dim elements not connected to the specified nodes
     * @param {Array} nodeIds - Node IDs to keep highlighted
     * @param {Array} edgeIds - Edge IDs to keep highlighted
     */
    dimUnconnectedElements(nodeIds, edgeIds) {
        const allNodes = this.nodes.get();
        const allEdges = this.edges.get();
        
        const nodeUpdates = allNodes.map(node => ({
            id: node.id,
            opacity: nodeIds.includes(node.id) ? 1 : 0.3
        }));
        
        const edgeUpdates = allEdges.map(edge => ({
            id: edge.id,
            opacity: edgeIds.includes(edge.id) ? 1 : 0.1
        }));
        
        this.nodes.update(nodeUpdates);
        this.edges.update(edgeUpdates);
    }
    
    /**
     * Clear all highlights
     */
    clearHighlights() {
        if (this.highlightedElements) {
            // Reset highlighted nodes
            const nodeUpdates = this.highlightedElements.nodes.map(id => {
                const originalNode = this.nodes.get(id);
                return {
                    id: id,
                    borderColor: this.getNodeBorderColor(originalNode.type),
                    borderWidth: 2
                };
            });
            
            // Reset highlighted edges to time-based colors
            const edgeUpdates = this.highlightedElements.edges.map(id => {
                const timestamp = this.edgeTimestamps.get(id);
                const color = this.calculateTimeGradientColor(timestamp);
                return {
                    id: id,
                    color: {
                        color: color,
                        highlight: '#00ff41',
                        hover: '#ff9900'
                    }
                };
            });
            
            this.nodes.update(nodeUpdates);
            this.edges.update(edgeUpdates);
            
            this.highlightedElements = null;
        }
    }
    /**
     * Clear hover highlights
     */
    clearHoverHighlights() {
        const allNodes = this.nodes.get();
        const allEdges = this.edges.get();
        
        const nodeUpdates = allNodes.map(node => ({ id: node.id, opacity: 1 }));
        const edgeUpdates = allEdges.map(edge => ({ id: edge.id, opacity: 1 }));
        
        this.nodes.update(nodeUpdates);
        this.edges.update(edgeUpdates);
    }
    /**
     * Highlight newly added elements
     * @param {Array} newNodes - New nodes
     * @param {Array} newEdges - New edges
     */
    highlightNewElements(newNodes, newEdges) {
        // Briefly highlight new nodes
        const nodeHighlights = newNodes.map(node => ({
            id: node.id,
            borderColor: '#00ff41',
            borderWidth: 4
        }));
        
        // Briefly highlight new edges
        const edgeHighlights = newEdges.map(edge => ({
            id: edge.id,
            color: '#00ff41',
            width: 4
        }));
        
        this.nodes.update(nodeHighlights);
        this.edges.update(edgeHighlights);
        
        // Reset after animation
        setTimeout(() => {
            const nodeResets = newNodes.map(node => ({
                id: node.id,
                borderColor: this.getNodeBorderColor(node.type),
                borderWidth: 2,
            }));
            
            // Reset edges to time-based colors
            const edgeResets = newEdges.map(edge => {
                const timestamp = this.edgeTimestamps.get(edge.id);
                const color = this.calculateTimeGradientColor(timestamp);
                return {
                    id: edge.id,
                    color: {
                        color: color,
                        highlight: '#00ff41',
                        hover: '#ff9900'
                    }
                };
            });
            
            this.nodes.update(nodeResets);
            this.edges.update(edgeResets);
        }, 2000);
    }
    /**
     * Handle stabilization completion
     */
    onStabilizationComplete() {
        console.log('Graph stabilization complete');
    }
    /**
     * Focus view on specific node
     * @param {string} nodeId - Node to focus on
     */
    focusOnNode(nodeId) {
        const nodePosition = this.network.getPositions([nodeId]);
        if (nodePosition[nodeId]) {
            this.network.moveTo({
                position: nodePosition[nodeId],
                scale: 1.5,
                animation: {
                    duration: 1000,
                    easingFunction: 'easeInOutQuart'
                }
            });
        }
    }
    
    /**
     * Toggle physics simulation
     */
    togglePhysics() {
        const currentPhysics = this.network.physics.physicsEnabled;
        this.network.setOptions({ physics: !currentPhysics });
        
        const button = document.getElementById('graph-physics');
        if (button) {
            button.textContent = currentPhysics ? '[PHYSICS OFF]' : '[PHYSICS ON]';
            button.style.color = currentPhysics ? '#ff9900' : '#00ff41';
        }
    }
    /**
     * Toggle node clustering
     */
    toggleClustering() {
        if (this.network.isCluster('domain-cluster')) {
            this.network.openCluster('domain-cluster');
        } else {
            const clusterOptions = {
                joinCondition: (nodeOptions) => {
                    return nodeOptions.type === 'domain';
                },
                clusterNodeProperties: {
                    id: 'domain-cluster',
                    label: 'Domains',
                    shape: 'database',
                    color: '#00ff41',
                    borderWidth: 3,
                }
            };
            this.network.cluster(clusterOptions);
        }
    }
    /**
     * Fit the view to show all nodes
     */
    fitView() {
        if (this.network) {
            this.network.fit({
                animation: {
                    duration: 1000,
                    easingFunction: 'easeInOutQuad'
                }
            });
        }
    }
    /**
     * Clear the graph
     */
    clear() {
        this.nodes.clear();
        this.edges.clear();
        this.edgeTimestamps.clear();
        this.history = [];
        this.largeEntityMembers.clear();
        this.initialTargetIds.clear();
        // Show placeholder
        const placeholder = this.container.querySelector('.graph-placeholder');
        if (placeholder) {
            placeholder.style.display = 'flex';
        }
    }
    /**
     * Show error message
     * @param {string} message - Error message
     */
    showError(message) {
        const placeholder = this.container.querySelector('.graph-placeholder .placeholder-text');
        if (placeholder) {
            placeholder.textContent = `Error: ${message}`;
            placeholder.style.color = '#ff6b6b';
        }
    }
     /* * @param {Set} excludedNodeIds - Node IDs to exclude from analysis (for simulation)
     * @param {Set} excludedEdgeTypes - Edge types to exclude from traversal
     * @param {Set} excludedNodeTypes - Node types to exclude from traversal
     * @returns {Object} Analysis results with reachable/unreachable nodes
     */
    analyzeGraphReachability(excludedNodeIds = new Set(), excludedEdgeTypes = new Set(), excludedNodeTypes = new Set()) {
        console.log("Performing comprehensive reachability analysis...");
        
        const analysis = {
            reachableNodes: new Set(),
            unreachableNodes: new Set(),
            isolatedClusters: [],
            affectedNodes: new Set()
        };
        if (this.nodes.length === 0) return analysis;
        // Build adjacency list excluding specified elements
        const adjacencyList = {};
        this.nodes.getIds().forEach(id => {
            if (!excludedNodeIds.has(id)) {
                adjacencyList[id] = [];
            }
        });
        this.edges.forEach(edge => {
            const edgeType = edge.metadata?.relationship_type || '';
            if (!excludedEdgeTypes.has(edgeType) && 
                !excludedNodeIds.has(edge.from) && 
                !excludedNodeIds.has(edge.to)) {
                
                if (adjacencyList[edge.from]) {
                    adjacencyList[edge.from].push(edge.to);
                }
            }
        });
        // BFS traversal from initial targets
        const traversalQueue = [];
        
        // Start from initial targets that aren't excluded
        this.initialTargetIds.forEach(rootId => {
            if (!excludedNodeIds.has(rootId)) {
                const node = this.nodes.get(rootId);
                if (node && !excludedNodeTypes.has(node.type)) {
                    if (!analysis.reachableNodes.has(rootId)) {
                        traversalQueue.push(rootId);
                        analysis.reachableNodes.add(rootId);
                    }
                }
            }
        });
        // BFS to find all reachable nodes
        let queueIndex = 0;
        while (queueIndex < traversalQueue.length) {
            const currentNode = traversalQueue[queueIndex++];
            
            for (const neighbor of (adjacencyList[currentNode] || [])) {
                if (!analysis.reachableNodes.has(neighbor)) {
                    const node = this.nodes.get(neighbor);
                    if (node && !excludedNodeTypes.has(node.type)) {
                        analysis.reachableNodes.add(neighbor);
                        traversalQueue.push(neighbor);
                    }
                }
            }
        }
        // Identify unreachable nodes (maintaining forensic integrity)
        Object.keys(adjacencyList).forEach(nodeId => {
            if (!analysis.reachableNodes.has(nodeId)) {
                analysis.unreachableNodes.add(nodeId);
            }
        });
        // Find isolated clusters among unreachable nodes
        analysis.isolatedClusters = this.findIsolatedClusters(
            Array.from(analysis.unreachableNodes), 
            adjacencyList
        );
        /*console.log(`Reachability analysis complete:`, {
            reachable: analysis.reachableNodes.size,
            unreachable: analysis.unreachableNodes.size,
            clusters: analysis.isolatedClusters.length
        });*/
        return analysis;
    }
    /**
     * Find isolated clusters within a set of nodes
     * Used for forensic analysis to identify disconnected subgraphs
     */
    findIsolatedClusters(nodeIds, adjacencyList) {
        const visited = new Set();
        const clusters = [];
        for (const nodeId of nodeIds) {
            if (!visited.has(nodeId)) {
                const cluster = [];
                const stack = [nodeId];
                
                while (stack.length > 0) {
                    const current = stack.pop();
                    if (!visited.has(current)) {
                        visited.add(current);
                        cluster.push(current);
                        
                        // Add unvisited neighbors within the unreachable set
                        for (const neighbor of (adjacencyList[current] || [])) {
                            if (nodeIds.includes(neighbor) && !visited.has(neighbor)) {
                                stack.push(neighbor);
                            }
                        }
                    }
                }
                
                if (cluster.length > 0) {
                    clusters.push(cluster);
                }
            }
        }
        return clusters;
    }
    /**
     * ENHANCED: Get comprehensive graph statistics with forensic information
     * Updates the existing getStatistics() method
     */
    getStatistics() {
        const basicStats = {
            nodeCount: this.nodes.length,
            edgeCount: this.edges.length,
        };
        // Add forensic statistics
        const visibleNodes = this.nodes.get({ filter: node => !node.hidden });
        const hiddenNodes = this.nodes.get({ filter: node => node.hidden });
        
        return {
            ...basicStats,
            forensicStatistics: {
                visibleNodes: visibleNodes.length,
                hiddenNodes: hiddenNodes.length,
                initialTargets: this.initialTargetIds.size,
                integrityStatus: visibleNodes.length > 0 && this.initialTargetIds.size > 0 ? 'INTACT' : 'COMPROMISED'
            }
        };
    }
    
    updateFilterControls() {
        if (!this.filterPanel) return;
        const nodeTypes = new Set(this.nodes.get().map(n => n.type));
        const edgeTypes = new Set(this.edges.get().map(e => e.metadata.relationship_type));
        // Wrap both columns in a single container with vertical layout
        let filterHTML = '';
        // Nodes section
        filterHTML += '
';
        // Edges section
        filterHTML += '
';
        filterHTML += '
 '; // Close filter-container
        this.filterPanel.innerHTML = filterHTML;
        this.filterPanel.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
            checkbox.addEventListener('change', () => this.applyAllFilters());
        });
    }
    /**
     * ENHANCED: Apply filters using consolidated reachability analysis
     * Replaces the existing applyAllFilters() method
     */
    applyAllFilters() {
        if (this.nodes.length === 0) return;
        // Get filter criteria from UI
        const excludedNodeTypes = new Set();
        this.filterPanel?.querySelectorAll('input[data-filter-type="node"]:not(:checked)').forEach(cb => {
            excludedNodeTypes.add(cb.value);
        });
        const excludedEdgeTypes = new Set();
        this.filterPanel?.querySelectorAll('input[data-filter-type="edge"]:not(:checked)').forEach(cb => {
            excludedEdgeTypes.add(cb.value);
        });
        // Perform comprehensive analysis
        const analysis = this.analyzeGraphReachability(new Set(), excludedEdgeTypes, excludedNodeTypes);
        // Apply visibility updates
        const nodeUpdates = this.nodes.map(node => ({
            id: node.id,
            hidden: !analysis.reachableNodes.has(node.id)
        }));
        const edgeUpdates = this.edges.map(edge => ({
            id: edge.id,
            hidden: excludedEdgeTypes.has(edge.metadata?.relationship_type || '') ||
                   !analysis.reachableNodes.has(edge.from) ||
                   !analysis.reachableNodes.has(edge.to)
        }));
        this.nodes.update(nodeUpdates);
        this.edges.update(edgeUpdates);
        console.log(`Enhanced filters applied. Visible nodes: ${analysis.reachableNodes.size}`);
    }
    /**
     * ENHANCED: Hide node with forensic integrity using reachability analysis
     * Replaces the existing hideNodeAndOrphans() method
     */
    hideNodeWithReachabilityAnalysis(nodeId) {
        console.log(`Hiding node ${nodeId} with reachability analysis...`);
        
        // Simulate hiding this node and analyze impact
        const excludedNodes = new Set([nodeId]);
        const analysis = this.analyzeGraphReachability(excludedNodes);
        
        // Nodes that will become unreachable (should be hidden)
        const nodesToHide = [nodeId, ...Array.from(analysis.unreachableNodes)];
        
        // Store history for potential revert
        const historyData = { 
            nodeIds: nodesToHide,
            operation: 'hide_with_reachability',
            timestamp: Date.now()
        };
        
        const updates = nodesToHide.map(id => ({ id: id, hidden: true }));
        this.nodes.update(updates);
        this.addToHistory('hide', historyData);
        return {
            hiddenNodes: nodesToHide,
            isolatedClusters: analysis.isolatedClusters
        };
    }
    /**
     * ENHANCED: Delete node with forensic integrity using reachability analysis
     * Replaces the existing deleteNodeAndOrphans() method
     */
    async deleteNodeWithReachabilityAnalysis(nodeId) {
        console.log(`Deleting node ${nodeId} with reachability analysis...`);
        
        // Simulate deletion and analyze impact
        const excludedNodes = new Set([nodeId]);
        const analysis = this.analyzeGraphReachability(excludedNodes);
        
        // Nodes that will become unreachable (should be deleted)
        const nodesToDelete = [nodeId, ...Array.from(analysis.unreachableNodes)];
        
        // Collect forensic data before deletion
        const historyData = {
            nodes: nodesToDelete.map(id => this.nodes.get(id)).filter(Boolean),
            edges: [],
            operation: 'delete_with_reachability',
            timestamp: Date.now(),
            forensicAnalysis: {
                originalTarget: nodeId,
                cascadeNodes: nodesToDelete.length - 1,
                isolatedClusters: analysis.isolatedClusters.length,
                clusterSizes: analysis.isolatedClusters.map(cluster => cluster.length)
            }
        };
        // Collect affected edges
        nodesToDelete.forEach(id => {
            const connectedEdgeIds = this.network.getConnectedEdges(id);
            const edges = this.edges.get(connectedEdgeIds);
            historyData.edges.push(...edges);
        });
        // Remove duplicates from edges
        historyData.edges = Array.from(new Map(historyData.edges.map(e => [e.id, e])).values());
        // Perform backend deletion with forensic logging
        let operationFailed = false;
        
        for (const targetNodeId of nodesToDelete) {
            try {
                const response = await fetch(`/api/graph/node/${targetNodeId}`, {
                    method: 'DELETE',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        forensicContext: {
                            operation: 'reachability_cascade_delete',
                            originalTarget: nodeId,
                            analysisTimestamp: historyData.timestamp
                        }
                    })
                });
                const result = await response.json();
                if (!result.success) {
                    console.error(`Backend deletion failed for node ${targetNodeId}:`, result.error);
                    operationFailed = true;
                    break;
                }
                
                console.log(`Node ${targetNodeId} deleted from backend with forensic context`);
                this.nodes.remove({ id: targetNodeId });
                
            } catch (error) {
                console.error(`API error during deletion of node ${targetNodeId}:`, error);
                operationFailed = true;
                break;
            }
        }
        // Handle operation results
        if (!operationFailed) {
            this.addToHistory('delete', historyData);            
            return {
                success: true,
                deletedNodes: nodesToDelete,
                forensicAnalysis: historyData.forensicAnalysis
            };
        } else {
            // Revert UI changes if backend operations failed - use update instead of add
            console.log("Reverting UI changes due to backend failure");
            this.nodes.update(historyData.nodes);
            this.edges.update(historyData.edges);
            
            return {
                success: false,
                error: "Backend deletion failed, UI reverted"
            };
        }
    }
    /**
     * Show context menu for a node
     * @param {string} nodeId - The ID of the node
     * @param {Event} event - The contextmenu event
     */
    showContextMenu(nodeId, event) {
        console.log('Showing context menu for node:', nodeId);
        const node = this.nodes.get(nodeId);
        
        // Create menu items
        let menuItems = `
            
                
                    
                    Focus on Node 
                 
        `;
        // Add "Iterate Scan" option only for domain or IP nodes
        if (node && (node.type === 'domain' || node.type === 'ip')) {
            const disabled = this.isScanning ? 'disabled' : ''; // Check if scanning
            const title = this.isScanning ? 'A scan is already in progress' : 'Iterate Scan (Add to Graph)'; // Add a title for disabled state
            menuItems += `
                
                    
                    Iterate Scan (Add to Graph) 
                 
            `;
        }
        menuItems += `
                
                    
                    Hide Node 
                 
                
                    
                    Delete Node 
                 
                
                    
                    Show Details 
                 
             
        `;
        
        this.contextMenu.innerHTML = menuItems;
        
        // Position the menu
        this.contextMenu.style.left = `${event.clientX}px`;
        this.contextMenu.style.top = `${event.clientY}px`;
        this.contextMenu.style.display = 'block';
        
        // Ensure menu stays within viewport
        const rect = this.contextMenu.getBoundingClientRect();
        if (rect.right > window.innerWidth) {
            this.contextMenu.style.left = `${event.clientX - rect.width}px`;
        }
        if (rect.bottom > window.innerHeight) {
            this.contextMenu.style.top = `${event.clientY - rect.height}px`;
        }
        // Add event listeners to menu items
        this.contextMenu.querySelectorAll('li').forEach(item => {
            item.addEventListener('click', (e) => {
                if (e.currentTarget.hasAttribute('disabled')) { // Prevent action if disabled
                    e.stopPropagation();
                    return;
                }
                e.stopPropagation();
                const action = e.currentTarget.dataset.action;
                const nodeId = e.currentTarget.dataset.nodeId;
                this.performContextMenuAction(action, nodeId);
                this.hideContextMenu();
            });
        });
    }
    /**
     * Hide the context menu
     */
    hideContextMenu() {
        if (this.contextMenu) {
            this.contextMenu.style.display = 'none';
        }
    }
    /**
     * UPDATED: Enhanced context menu actions using new methods
     * Updates the existing performContextMenuAction() method
     */
    performContextMenuAction(action, nodeId) {        
        switch (action) {
            case 'focus':
                this.focusOnNode(nodeId);
                break;
                
            case 'iterate':
                const event = new CustomEvent('iterateScan', {
                    detail: { nodeId }
                });
                document.dispatchEvent(event);
                break;
                
            case 'hide':
                // Use enhanced method with reachability analysis
                this.hideNodeWithReachabilityAnalysis(nodeId);
                break;
                
            case 'delete':
                // Use enhanced method with reachability analysis
                this.deleteNodeWithReachabilityAnalysis(nodeId);
                break;
                
            case 'details':
                const node = this.nodes.get(nodeId);
                if (node) {
                    this.showNodeDetails(node);
                }
                break;
                
            default:
                console.warn('Unknown action:', action);
        }
    }
    
    /**
     * Add an operation to the history stack
     * @param {string} type - The type of operation ('hide', 'delete')
     * @param {Object} data - The data needed to revert the operation
     */
    addToHistory(type, data) {
        this.history.push({ type, data });
    }
    /**
     * Revert the last action
     */
    async revertLastAction() {
        const lastAction = this.history.pop();
        if (!lastAction) {
            console.log('No actions to revert.');
            return;
        }
    
        switch (lastAction.type) {
            case 'hide':
                // Revert hiding nodes by un-hiding them
                const updates = lastAction.data.nodeIds.map(id => ({ id: id, hidden: false }));
                this.nodes.update(updates);
                break;
            case 'delete':
                try {
                    const response = await fetch('/api/graph/revert', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                        },
                        body: JSON.stringify(lastAction)
                    });
                    const result = await response.json();
    
                    if (result.success) {
                        console.log('Delete action reverted successfully on backend.');
                        // Re-add all nodes and edges from the history to the local view - use update instead of add
                        this.nodes.update(lastAction.data.nodes);
                        this.edges.update(lastAction.data.edges);
                    } else {
                        console.error('Failed to revert delete action on backend:', result.error);
                        // Push the action back onto the history stack if the API call failed
                        this.history.push(lastAction);
                    }
                } catch (error) {
                    console.error('Error during revert API call:', error);
                    this.history.push(lastAction);
                }
                break;
        }
    }
    /**
     * FIXED: Unhide all hidden nodes, excluding large entity members and disconnected nodes.
     * This prevents orphaned large entity members from appearing as free-floating nodes.
     */
    unhideAll() {
        const allHiddenNodes = this.nodes.get({
            filter: (node) => {
                // Skip nodes that are part of a large entity
                if (node.metadata && node.metadata.large_entity_id) {
                    return false;
                }
                
                // Skip nodes that are not hidden
                if (node.hidden !== true) {
                    return false;
                }
                
                // Skip nodes that have no edges (would appear disconnected)
                const nodeId = node.id;
                const hasIncomingEdges = this.edges.get().some(edge => edge.to === nodeId && !edge.hidden);
                const hasOutgoingEdges = this.edges.get().some(edge => edge.from === nodeId && !edge.hidden);
                
                if (!hasIncomingEdges && !hasOutgoingEdges) {
                    console.log(`Skipping disconnected node ${nodeId} from unhide`);
                    return false;
                }
                
                return true;
            }
        });
        
        if (allHiddenNodes.length > 0) {
            console.log(`Unhiding ${allHiddenNodes.length} nodes with valid connections`);
            const updates = allHiddenNodes.map(node => ({ id: node.id, hidden: false }));
            this.nodes.update(updates);
        } else {
            console.log('No eligible nodes to unhide');
        }
    }
    
}
// Export for use in main.js
window.GraphManager = GraphManager;