// Topology Graph Component with D3.js force-directed visualization
class TopologyGraphComponent extends Component {
    constructor(container, viewModel, eventBus) {
        super(container, viewModel, eventBus);
        logger.debug('TopologyGraphComponent: Constructor called');
        this.svg = null;
        this.simulation = null;
        this.zoom = null;
        this.width = 0;  // Will be set dynamically based on container size
        this.height = 0;  // Will be set dynamically based on container size
        this.isInitialized = false;

        // Drawer state for desktop reuse (shared singleton)
        this.drawer = new DrawerComponent();

        // Tooltip for labels on hover
        this.tooltipEl = null;

        // Track drag state to defer updates
        this.isDragging = false;
        this.pendingUpdate = null;
        this.draggedNodePositions = new Map(); // Track final positions of dragged nodes
    }

    // Determine desktop threshold
    isDesktop() {
        return this.drawer.isDesktop();
    }


    openDrawerForNode(nodeData) {
        // Get display name for drawer title
        let displayName = 'Node Details';
        try {
            const hostname = nodeData.hostname || '';
            const ip = nodeData.ip || '';
            
            if (hostname && ip) {
                displayName = `${hostname} - ${ip}`;
            } else if (hostname) {
                displayName = hostname;
            } else if (ip) {
                displayName = ip;
            }
        } catch (_) {}

        // Open drawer with content callback
        this.drawer.openDrawer(displayName, (contentContainer, setActiveComponent) => {
            // Mount NodeDetailsComponent
            const nodeDetailsVM = new NodeDetailsViewModel();
            const nodeDetailsComponent = new NodeDetailsComponent(contentContainer, nodeDetailsVM, this.eventBus);
            setActiveComponent(nodeDetailsComponent);

            const ip = nodeData.ip || nodeData.id;
            nodeDetailsVM.loadNodeDetails(ip).then(() => {
                nodeDetailsComponent.mount();
            }).catch((error) => {
                logger.error('Failed to load node details (topology drawer):', error);
                contentContainer.innerHTML = `
                    <div class="error">
                        <strong>Error loading node details:</strong><br>
                        ${this.escapeHtml(error.message)}
                    </div>
                `;
            });
        });

        try {
            if (window.TerminalPanel && typeof nodeData.ip === 'string') {
                window.TerminalPanel.lastNodeIp = nodeData.ip;
                if (window.TerminalPanel._updateTitle) {
                    window.TerminalPanel._updateTitle(nodeData.ip);
                }
            }
        } catch (_) {}
    }

    closeDrawer() {
        this.drawer.closeDrawer();
    }

    // Tooltip helpers
    ensureTooltip() {
        if (this.tooltipEl) return;
        const el = document.createElement('div');
        el.className = 'topology-tooltip';
        document.body.appendChild(el);
        this.tooltipEl = el;
    }

    showTooltip(nodeData, pageX, pageY) {
        this.ensureTooltip();
        const labels = (nodeData && nodeData.labels) ? nodeData.labels : ((nodeData && nodeData.resources) ? nodeData.resources : null);
        if (!labels || Object.keys(labels).length === 0) {
            this.hideTooltip();
            return;
        }
        const chips = Object.entries(labels)
            .map(([k, v]) => `<span class=\"label-chip\">${this.escapeHtml(String(k))}: ${this.escapeHtml(String(v))}</span>`)
            .join('');
        
        // Add hint for node interactions
        let hint = '<div style="margin-top: 8px; font-size: 11px; color: rgba(255, 255, 255, 0.7); text-align: center;">💡 Click to set as API Node & toggle view</div>';
        if (nodeData && nodeData.isPrimary) {
            hint = '<div style="margin-top: 8px; font-size: 11px; color: rgba(255, 215, 0, 0.9); text-align: center;">⭐ API Node - Click to toggle view</div>';
        }
        
        this.tooltipEl.innerHTML = `<div class=\"member-labels\">${chips}</div>${hint}`;
        this.positionTooltip(pageX, pageY);
        this.tooltipEl.classList.add('visible');
    }

    positionTooltip(pageX, pageY) {
        if (!this.tooltipEl) return;
        const offset = 12;
        let left = pageX + offset;
        let top = pageY + offset;
        const { innerWidth, innerHeight } = window;
        const rect = this.tooltipEl.getBoundingClientRect();
        if (left + rect.width > innerWidth - 8) left = pageX - rect.width - offset;
        if (top + rect.height > innerHeight - 8) top = pageY - rect.height - offset;
        this.tooltipEl.style.left = `${Math.max(8, left)}px`;
        this.tooltipEl.style.top = `${Math.max(8, top)}px`;
    }

    moveTooltip(pageX, pageY) {
        if (!this.tooltipEl || !this.tooltipEl.classList.contains('visible')) return;
        this.positionTooltip(pageX, pageY);
    }

    hideTooltip() {
        if (this.tooltipEl) this.tooltipEl.classList.remove('visible');
    }

    updateDimensions(container) {
        // Get the container's actual dimensions
        const rect = container.getBoundingClientRect();
        this.width = rect.width || 1400;  // Fallback to 1400 if width is 0
        this.height = rect.height || 1000; // Fallback to 1000 if height is 0
        
        // Ensure minimum dimensions
        this.width = Math.max(this.width, 800);
        this.height = Math.max(this.height, 600);
        
        logger.debug('TopologyGraphComponent: Updated dimensions to', this.width, 'x', this.height);
    }

    handleResize() {
        // Debounce resize events to avoid excessive updates
        if (this.resizeTimeout) {
            clearTimeout(this.resizeTimeout);
        }
        
        this.resizeTimeout = setTimeout(() => {
            const container = this.findElement('#topology-graph-container');
            if (container && this.svg) {
                this.updateDimensions(container);
                // Update SVG viewBox and force center
                this.svg.attr('viewBox', `0 0 ${this.width} ${this.height}`);
                if (this.simulation) {
                    this.simulation.force('center', d3.forceCenter(this.width / 2, this.height / 2));
                    this.simulation.alpha(0.3).restart();
                }
            }
        }, 250); // 250ms debounce
    }

    // Override mount to ensure proper initialization
    mount() {
        if (this.isMounted) return;
        
        logger.debug('TopologyGraphComponent: Starting mount...');
        logger.debug('TopologyGraphComponent: isInitialized =', this.isInitialized);
        
        // Call initialize if not already done
        if (!this.isInitialized) {
            logger.debug('TopologyGraphComponent: Initializing during mount...');
            this.initialize().then(() => {
                logger.debug('TopologyGraphComponent: Initialization completed, calling completeMount...');
                // Complete mount after initialization
                this.completeMount();
            }).catch(error => {
                logger.error('TopologyGraphComponent: Initialization failed during mount:', error);
                // Still complete mount to prevent blocking
                this.completeMount();
            });
        } else {
            logger.debug('TopologyGraphComponent: Already initialized, calling completeMount directly...');
            this.completeMount();
        }
    }

    completeMount() {
        logger.debug('TopologyGraphComponent: completeMount called');
        this.isMounted = true;
        logger.debug('TopologyGraphComponent: Setting up event listeners...');
        this.setupEventListeners();
        logger.debug('TopologyGraphComponent: Setting up view model listeners...');
        this.setupViewModelListeners();
        logger.debug('TopologyGraphComponent: Calling render...');
        this.render();
        
        logger.debug('TopologyGraphComponent: Mounted successfully');
    }

    setupEventListeners() {
        logger.debug('TopologyGraphComponent: setupEventListeners called');
        logger.debug('TopologyGraphComponent: Container:', this.container);
        logger.debug('TopologyGraphComponent: Container ID:', this.container?.id);
        
        // Add resize listener to update dimensions when window is resized
        this.resizeHandler = this.handleResize.bind(this);
        window.addEventListener('resize', this.resizeHandler);
        
        // Refresh button removed from HTML, so no need to set up event listeners
        logger.debug('TopologyGraphComponent: No event listeners needed (refresh button removed)');
    }

    setupViewModelListeners() {
        logger.debug('TopologyGraphComponent: setupViewModelListeners called');
        logger.debug('TopologyGraphComponent: isInitialized =', this.isInitialized);
        
        if (this.isInitialized) {
            // Component is already initialized, set up subscriptions immediately
            logger.debug('TopologyGraphComponent: Setting up property subscriptions immediately');
            this.subscribeToProperty('nodes', this.renderGraph.bind(this));
            this.subscribeToProperty('links', this.renderGraph.bind(this));
            this.subscribeToProperty('isLoading', this.handleLoadingState.bind(this));
            this.subscribeToProperty('error', this.handleError.bind(this));
            this.subscribeToProperty('selectedNode', this.updateSelection.bind(this));
            this.subscribeToProperty('discoveryEvent', this.handleDiscoveryEvent.bind(this));
        } else {
            // Component not yet initialized, store for later
            logger.debug('TopologyGraphComponent: Component not initialized, storing pending subscriptions');
            this._pendingSubscriptions = [
                ['nodes', this.renderGraph.bind(this)],
                ['links', this.renderGraph.bind(this)],
                ['isLoading', this.handleLoadingState.bind(this)],
                ['error', this.handleError.bind(this)],
                ['selectedNode', this.updateSelection.bind(this)],
                ['discoveryEvent', this.handleDiscoveryEvent.bind(this)]
            ];
        }
    }

    async initialize() {
        logger.debug('TopologyGraphComponent: Initializing...');
        
        // Wait for DOM to be ready
        if (document.readyState === 'loading') {
            await new Promise(resolve => {
                document.addEventListener('DOMContentLoaded', resolve);
            });
        }
        
        // Set up the SVG container
        this.setupSVG();
        
        // Mark as initialized
        this.isInitialized = true;
        
        // Now set up the actual property listeners after initialization
        if (this._pendingSubscriptions) {
            this._pendingSubscriptions.forEach(([property, callback]) => {
                this.subscribeToProperty(property, callback);
            });
            this._pendingSubscriptions = null;
        }
        
        // Set up WebSocket listeners for real-time updates
        this.setupWebSocketListeners();
        
        // Initial data load
        await this.viewModel.updateNetworkTopology();
    }

    setupWebSocketListeners() {
        logger.debug('TopologyGraphComponent: Setting up WebSocket listeners...');
        
        // The view model handles WebSocket events and updates its state
        // The component will automatically re-render via view model subscriptions
        if (this.viewModel && typeof this.viewModel.setupWebSocketListeners === 'function') {
            this.viewModel.setupWebSocketListeners();
            logger.debug('TopologyGraphComponent: WebSocket listeners configured');
        } else {
            logger.warn('TopologyGraphComponent: View model does not support WebSocket listeners');
        }
    }

    setupSVG() {
        const container = this.findElement('#topology-graph-container');
        if (!container) {
            logger.error('TopologyGraphComponent: Graph container not found');
            return;
        }

        // Calculate dynamic dimensions based on container size
        this.updateDimensions(container);

        // Clear existing content
        container.innerHTML = '';

        // Create a wrapper for SVG and controls
        const wrapper = document.createElement('div');
        wrapper.style.position = 'relative';
        wrapper.style.width = '100%';
        wrapper.style.height = '100%';
        container.appendChild(wrapper);

        // Add rearrange button
        this.createRearrangeButton(wrapper);

        // Create SVG element
        this.svg = d3.select(wrapper)
            .append('svg')
            .attr('width', '100%')
            .attr('height', '100%')
            .attr('viewBox', `0 0 ${this.width} ${this.height}`)
            .style('border', '1px solid rgba(255, 255, 255, 0.1)')
            .style('border-radius', '12px');


        // Add zoom behavior
        this.zoom = d3.zoom()
            .scaleExtent([0.5, 5])
            .on('zoom', (event) => {
                this.svg.select('g').attr('transform', event.transform);
            });

        this.svg.call(this.zoom);

        // Create main group for zoom and apply initial zoom
        const mainGroup = this.svg.append('g');
        
        // Apply initial zoom to show the graph more zoomed in
        mainGroup.attr('transform', 'scale(1.4) translate(-200, -150)');

        logger.debug('TopologyGraphComponent: SVG setup completed');
    }

    createRearrangeButton(container) {
        const button = document.createElement('button');
        button.className = 'topology-rearrange-btn';
        button.innerHTML = `
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <circle cx="12" cy="5" r="1.5"/>
                <circle cx="12" cy="12" r="1.5"/>
                <circle cx="12" cy="19" r="1.5"/>
                <circle cx="5" cy="12" r="1.5"/>
                <circle cx="19" cy="12" r="1.5"/>
                <path d="M12 7v3m0 2v3m-5-3h3m2 0h3"/>
            </svg>
            <span>Rearrange</span>
        `;
        button.title = 'Rearrange nodes into a clean layout';
        button.style.cssText = `
            position: absolute;
            left: 16px;
            top: 16px;
            z-index: 10;
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 8px 14px;
            background: var(--card-background, rgba(30, 30, 30, 0.95));
            border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
            border-radius: 8px;
            color: var(--text-primary, #ecf0f1);
            font-size: 13px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s ease;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
        `;

        // Add hover effect
        button.addEventListener('mouseenter', () => {
            button.style.background = 'var(--card-hover, rgba(40, 40, 40, 0.95))';
            button.style.borderColor = 'var(--primary-color, #3498db)';
            button.style.transform = 'translateY(-1px)';
            button.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
        });

        button.addEventListener('mouseleave', () => {
            button.style.background = 'var(--card-background, rgba(30, 30, 30, 0.95))';
            button.style.borderColor = 'var(--border-color, rgba(255, 255, 255, 0.1))';
            button.style.transform = 'translateY(0)';
            button.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.2)';
        });

        button.addEventListener('click', () => {
            this.rearrangeNodes();
        });

        container.appendChild(button);
    }

    rearrangeNodes() {
        logger.debug('TopologyGraphComponent: Rearranging nodes');

        // Clear all manually dragged positions
        this.draggedNodePositions.clear();
        logger.debug('TopologyGraphComponent: Cleared dragged positions');

        if (!this.simulation) {
            logger.warn('TopologyGraphComponent: No simulation to rearrange');
            return;
        }

        // Get current nodes and reset their fixed positions
        const nodes = this.simulation.nodes();
        nodes.forEach(node => {
            node.fx = null;
            node.fy = null;
            // Give them a slight random velocity to help spread out
            node.vx = (Math.random() - 0.5) * 50;
            node.vy = (Math.random() - 0.5) * 50;
        });

        // Restart the simulation with high alpha for a fresh layout
        this.simulation
            .alpha(1)
            .alphaTarget(0)
            .restart();

        logger.debug('TopologyGraphComponent: Simulation restarted for rearrangement');
    }

    // Ensure component is initialized
    async ensureInitialized() {
        if (!this.isInitialized) {
            logger.debug('TopologyGraphComponent: Ensuring initialization...');
            await this.initialize();
        }
        return this.isInitialized;
    }

    renderGraph() {
        try {
            // Check if component is initialized
            if (!this.isInitialized) {
                logger.debug('TopologyGraphComponent: Component not yet initialized, ensuring initialization...');
                this.ensureInitialized().then(() => {
                    // Re-render after initialization
                    this.renderGraph();
                }).catch(error => {
                    logger.error('TopologyGraphComponent: Failed to initialize:', error);
                });
                return;
            }

            const nodes = this.viewModel.get('nodes');
            const links = this.viewModel.get('links');

            // Defer updates while dragging
            if (this.isDragging) {
                logger.debug('TopologyGraphComponent: Drag in progress, deferring update');
                this.pendingUpdate = { nodes, links };
                return;
            }

            // Clear any pending update since we're processing now
            this.pendingUpdate = null;

            // Check if SVG is initialized
            if (!this.svg) {
                logger.debug('TopologyGraphComponent: SVG not initialized yet, setting up SVG first');
                this.setupSVG();
            }

            if (!nodes || nodes.length === 0) {
                this.showNoData();
                return;
            }

            logger.debug('TopologyGraphComponent: Rendering graph with', nodes.length, 'nodes and', links.length, 'links');

            // Get the main SVG group (the one created in setupSVG)
            let svgGroup = this.svg.select('g');
            if (!svgGroup || svgGroup.empty()) {
                logger.debug('TopologyGraphComponent: Creating new SVG group');
                svgGroup = this.svg.append('g');
                // Apply initial zoom to show the graph more zoomed in
                svgGroup.attr('transform', 'scale(1.4) translate(-200, -150)');
            }

            // Use D3's enter/exit pattern for smooth dynamic updates
            this.updateLinks(svgGroup, links);
            this.updateNodes(svgGroup, nodes);
            this.updateLinkLabels(svgGroup, links);
            
            // Update or create simulation
            this.updateSimulation(nodes, links, svgGroup);

            this.addLegend(svgGroup);
        } catch (error) {
            logger.error('Failed to render graph:', error);
        }
    }

    updateLinks(svgGroup, links) {
        // Get or create link group
        let linkGroup = svgGroup.select('.link-group');
        if (linkGroup.empty()) {
            linkGroup = svgGroup.append('g').attr('class', 'link-group graph-element');
        }

        // Bind data with key function for proper enter/exit
        // D3's forceLink mutates source/target from strings to objects, so we need to normalize the key
        const link = linkGroup.selectAll('line')
            .data(links, d => {
                const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
                const targetId = typeof d.target === 'object' ? d.target.id : d.target;
                // For directional links, maintain source -> target order in the key
                // For bidirectional links, use consistent ordering to avoid duplicates
                if (d.bidirectional) {
                    return sourceId < targetId ? `${sourceId}-${targetId}` : `${targetId}-${sourceId}`;
                } else {
                    return `${sourceId}->${targetId}`;
                }
            });

        // Remove old links
        link.exit()
            .transition()
            .duration(600)
            .ease(d3.easeCubicOut)
            .style('opacity', 0)
            .remove();

        // Add new links
        const linkEnter = link.enter().append('line')
            .attr('stroke-opacity', 0);

        // Merge and update all links
        const linkMerge = linkEnter.merge(link)
            .transition()
            .duration(600)
            .ease(d3.easeCubicInOut)
            .attr('stroke', d => this.getLinkColor(d.latency))
            .attr('stroke-opacity', 0.7)
            .attr('stroke-width', d => Math.max(1, Math.min(3, d.latency / 8)));

        // Store reference for simulation
        this.linkSelection = linkGroup.selectAll('line');

        // Add interactions to links
        this.linkSelection
            .on('mouseover', (event, d) => {
                d3.select(event.currentTarget)
                    .attr('stroke-width', d => Math.max(2, Math.min(4, d.latency / 6)))
                    .attr('stroke-opacity', 0.9);
            })
            .on('mouseout', (event, d) => {
                d3.select(event.currentTarget)
                    .attr('stroke-width', d => Math.max(1, Math.min(3, d.latency / 8)))
                    .attr('stroke-opacity', 0.7);
            });
    }

    updateNodes(svgGroup, nodes) {
        // Get or create node group
        let nodeGroup = svgGroup.select('.node-group');
        if (nodeGroup.empty()) {
            nodeGroup = svgGroup.append('g').attr('class', 'node-group graph-element');
        }

        // Merge live simulation positions with new data
        if (this.simulation) {
            const simulationNodes = this.simulation.nodes();
            const simNodeMap = new Map(simulationNodes.map(n => [n.id, n]));
            
            nodes.forEach(node => {
                const simNode = simNodeMap.get(node.id);
                if (simNode) {
                    // Keep simulation's position data (source of truth)
                    node.x = simNode.x;
                    node.y = simNode.y;
                    node.vx = simNode.vx;
                    node.vy = simNode.vy;
                    node.fx = simNode.fx;
                    node.fy = simNode.fy;
                }
                
                // Apply saved dragged positions (always pin these)
                const draggedPos = this.draggedNodePositions.get(node.id);
                if (draggedPos) {
                    node.x = draggedPos.x;
                    node.y = draggedPos.y;
                }
            });
        }

        // Bind data with key function
        const node = nodeGroup.selectAll('g.node')
            .data(nodes, d => d.id);

        // Remove old nodes and clean up their dragged positions
        node.exit()
            .each((d) => {
                // Clean up dragged position data for removed nodes
                this.draggedNodePositions.delete(d.id);
                logger.debug(`TopologyGraphComponent: Cleaned up dragged position for removed node ${d.id}`);
            })
            .transition()
            .duration(300)
            .style('opacity', 0)
            .remove();

        // Add new nodes
        const nodeEnter = node.enter().append('g')
            .attr('class', 'node')
            .style('opacity', 0)
            .call(this.drag(this.simulation));

        // Add circles to new nodes
        nodeEnter.append('circle')
            .attr('r', d => this.getNodeRadius(d.status))
            .attr('fill', d => this.getNodeColor(d.status))
            .attr('stroke', d => d.isPrimary ? '#FFD700' : '#fff')
            .attr('stroke-width', d => d.isPrimary ? 3 : 2);

        // Status indicator - removed
        
        // Primary node badge
        const primaryBadge = nodeEnter.filter(d => d.isPrimary)
            .append('g')
            .attr('class', 'primary-badge')
            .attr('transform', 'translate(0, 0)');
        
        primaryBadge.append('text')
            .text('A')
            .attr('text-anchor', 'middle')
            .attr('dominant-baseline', 'central')
            .attr('font-size', '12px')
            .attr('font-weight', 'bold')
            .attr('fill', '#000');

        // Hostname
        nodeEnter.append('text')
            .attr('class', 'hostname-text')
            .text(d => d.hostname.length > 15 ? d.hostname.substring(0, 15) + '...' : d.hostname)
            .attr('x', 15)
            .attr('y', 4)
            .attr('font-size', '13px')
            .attr('fill', 'var(--text-primary)')
            .attr('font-weight', '500');

        // IP
        nodeEnter.append('text')
            .attr('class', 'ip-text')
            .text(d => d.ip)
            .attr('x', 15)
            .attr('y', 22)
            .attr('font-size', '11px')
            .attr('fill', 'var(--text-secondary)');

        // App label
        nodeEnter.append('text')
            .attr('class', 'app-text')
            .text(d => (d.labels && d.labels.app) ? String(d.labels.app) : '')
            .attr('x', 15)
            .attr('y', 38)
            .attr('font-size', '11px')
            .attr('fill', 'var(--text-secondary)')
            .attr('font-weight', '500')
            .attr('display', d => (d.labels && d.labels.app) ? null : 'none');

        // Status text
        nodeEnter.append('text')
            .attr('class', 'status-text')
            .text(d => d.status)
            .attr('x', 15)
            .attr('y', 56)
            .attr('font-size', '11px')
            .attr('fill', d => this.getNodeColor(d.status))
            .attr('font-weight', '600');

        // Merge and update all nodes
        const nodeMerge = nodeEnter.merge(node);
        
        // Update existing node properties with transition
        nodeMerge.select('circle:first-child')
            .transition()
            .duration(300)
            .attr('r', d => this.getNodeRadius(d.status))
            .attr('fill', d => this.getNodeColor(d.status))
            .attr('stroke', d => d.isPrimary ? '#FFD700' : '#fff')
            .attr('stroke-width', d => d.isPrimary ? 3 : 2);

        // Update status indicator - removed

        nodeMerge.select('.hostname-text')
            .text(d => d.hostname.length > 15 ? d.hostname.substring(0, 15) + '...' : d.hostname);

        nodeMerge.select('.ip-text')
            .text(d => d.ip);

        nodeMerge.select('.app-text')
            .text(d => (d.labels && d.labels.app) ? String(d.labels.app) : '')
            .attr('display', d => (d.labels && d.labels.app) ? null : 'none');

        nodeMerge.select('.status-text')
            .text(d => d.status)
            .transition()
            .duration(300)
            .attr('fill', d => this.getNodeColor(d.status));

        // Update primary badge for existing nodes
        // Remove badge from nodes that are no longer primary
        nodeMerge.filter(d => !d.isPrimary)
            .select('.primary-badge')
            .remove();
        
        // Add badge to nodes that became primary (if they don't already have one)
        nodeMerge.filter(d => d.isPrimary)
            .each(function(d) {
                const nodeGroup = d3.select(this);
                if (nodeGroup.select('.primary-badge').empty()) {
                    const badge = nodeGroup.append('g')
                        .attr('class', 'primary-badge')
                        .attr('transform', 'translate(0, 0)');
                    
                    badge.append('text')
                        .text('A')
                        .attr('text-anchor', 'middle')
                        .attr('dominant-baseline', 'central')
                        .attr('font-size', '12px')
                        .attr('font-weight', 'bold')
                        .attr('fill', '#000');
                }
            });

        // Fade in only new nodes (not existing ones)
        nodeEnter.transition()
            .duration(300)
            .style('opacity', 1);
        
        // Ensure existing nodes remain visible
        node.style('opacity', 1);

        // Store reference for simulation
        this.nodeSelection = nodeMerge;

        // Add interactions
        this.nodeSelection
            .on('click', async (event, d) => {
                event.stopPropagation();
                
                // Check if we're toggling back to mesh (clicking same center node in star mode)
                const currentMode = this.viewModel.get('topologyMode');
                const currentCenter = this.viewModel.get('starCenterNode');
                const togglingToMesh = currentMode === 'star' && currentCenter === d.ip;
                
                // Toggle topology mode - switch between mesh and star
                await this.viewModel.toggleTopologyMode(d.ip);
                
                // Also switch to this node as primary (if not already)
                if (!d.isPrimary) {
                    try {
                        logger.info(`TopologyGraphComponent: Switching primary to ${d.ip}`);
                        await this.viewModel.switchToPrimaryNode(d.ip);
                    } catch (error) {
                        logger.error('TopologyGraphComponent: Failed to switch primary:', error);
                    }
                }
                
                // Open or close drawer based on mode toggle
                if (togglingToMesh) {
                    // Switching back to mesh - close drawer
                    this.viewModel.clearSelection();
                    if (this.isDesktop()) {
                        this.closeDrawer();
                    }
                } else {
                    // Switching to star or changing center - open drawer
                    this.viewModel.selectNode(d.id);
                    this.updateSelection(d.id);
                    if (this.isDesktop()) {
                        this.openDrawerForNode(d);
                    } else {
                        this.showMemberCardOverlay(d);
                    }
                }
            })
            .on('mouseover', (event, d) => {
                d3.select(event.currentTarget).select('circle')
                    .attr('r', d => this.getNodeRadius(d.status) + 4)
                    .attr('stroke-width', 3);
                this.showTooltip(d, event.pageX, event.pageY);
            })
            .on('mouseout', (event, d) => {
                d3.select(event.currentTarget).select('circle')
                    .attr('r', d => this.getNodeRadius(d.status))
                    .attr('stroke-width', 2);
                this.hideTooltip();
            })
            .on('mousemove', (event, d) => {
                this.moveTooltip(event.pageX, event.pageY);
            });
    }

    updateLinkLabels(svgGroup, links) {
        const topologyMode = this.viewModel.get('topologyMode') || 'mesh';
        
        // Only show latency labels in star mode
        if (topologyMode !== 'star') {
            // Clear any existing labels in mesh mode
            const linkLabelGroup = svgGroup.select('.link-label-group');
            if (!linkLabelGroup.empty()) {
                linkLabelGroup.selectAll('text').remove();
            }
            this.linkLabelSelection = null;
            return;
        }
        
        // Star mode - show latency labels
        // Get or create link label group
        let linkLabelGroup = svgGroup.select('.link-label-group');
        if (linkLabelGroup.empty()) {
            linkLabelGroup = svgGroup.append('g').attr('class', 'link-label-group graph-element');
        }

        // Bind data with same key function as updateLinks for consistency
        const linkLabels = linkLabelGroup.selectAll('text')
            .data(links, d => {
                const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
                const targetId = typeof d.target === 'object' ? d.target.id : d.target;
                return sourceId < targetId ? `${sourceId}-${targetId}` : `${targetId}-${sourceId}`;
            });

        // Remove old labels
        linkLabels.exit()
            .transition()
            .duration(300)
            .style('opacity', 0)
            .remove();

        // Add new labels
        const linkLabelsEnter = linkLabels.enter().append('text')
            .attr('font-size', '12px')
            .attr('fill', 'var(--text-primary)')
            .attr('font-weight', '600')
            .attr('text-anchor', 'middle')
            .style('text-shadow', '0 1px 2px rgba(0, 0, 0, 0.8)')
            .style('opacity', 0);

        // Merge and update
        linkLabelsEnter.merge(linkLabels)
            .text(d => `${d.latency}ms`)
            .transition()
            .duration(300)
            .style('opacity', 1);

        // Store reference for simulation
        this.linkLabelSelection = linkLabelGroup.selectAll('text');
    }

    updateSimulation(nodes, links, svgGroup) {
        if (!this.simulation) {
            // Create new simulation
            this.simulation = d3.forceSimulation(nodes)
                .force('link', d3.forceLink(links).id(d => d.id).distance(300))
                .force('charge', d3.forceManyBody().strength(-800))
                .force('center', d3.forceCenter(this.width / 2, this.height / 2))
                .force('collision', d3.forceCollide().radius(80));

            // Set up tick handler
            this.simulation.on('tick', () => {
                // Update link positions
                if (this.linkSelection) {
                    this.linkSelection
                        .attr('x1', d => d.source.x)
                        .attr('y1', d => d.source.y)
                        .attr('x2', d => d.target.x)
                        .attr('y2', d => d.target.y);
                }

                // Update link label positions
                if (this.linkLabelSelection) {
                    this.linkLabelSelection
                        .attr('x', d => (d.source.x + d.target.x) / 2)
                        .attr('y', d => (d.source.y + d.target.y) / 2 - 5);
                }

                // Update node positions
                if (this.nodeSelection) {
                    this.nodeSelection
                        .attr('transform', d => `translate(${d.x},${d.y})`);
                }
            });
        } else {
            // Don't update simulation if user is dragging
            if (this.isDragging) {
                logger.debug('TopologyGraphComponent: Skipping simulation update during drag');
                return;
            }

            // Check if this is a structural change (nodes/links added/removed)
            const currentNodes = this.simulation.nodes();
            const currentNodeIds = new Set(currentNodes.map(n => n.id));
            const newNodeIds = new Set(nodes.map(n => n.id));
            
            // Check if nodes changed
            const nodesChanged = currentNodes.length !== nodes.length ||
                                [...newNodeIds].some(id => !currentNodeIds.has(id)) ||
                                [...currentNodeIds].some(id => !newNodeIds.has(id));
            
            // Check if links changed (compare link keys)
            const currentLinks = this.simulation.force('link').links();
            const currentLinkKeys = new Set(currentLinks.map(l => {
                const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
                const targetId = typeof l.target === 'object' ? l.target.id : l.target;
                return `${sourceId}->${targetId}`;
            }));
            const newLinkKeys = new Set(links.map(l => {
                const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
                const targetId = typeof l.target === 'object' ? l.target.id : l.target;
                return `${sourceId}->${targetId}`;
            }));
            
            const linksChanged = currentLinks.length !== links.length ||
                                [...newLinkKeys].some(key => !currentLinkKeys.has(key)) ||
                                [...currentLinkKeys].some(key => !newLinkKeys.has(key));
            
            const isStructuralChange = nodesChanged || linksChanged;
            
            // Update simulation data
            this.simulation.nodes(nodes);
            this.simulation.force('link').links(links);
            
            if (isStructuralChange) {
                if (linksChanged && !nodesChanged) {
                    // Only links changed (e.g., primary node switch)
                    // Keep nodes in place, just update link positions
                    logger.debug('TopologyGraphComponent: Link structure changed (primary switch), keeping node positions');
                    
                    // Stop the simulation completely
                    this.simulation.stop();
                    
                    // Trigger a single tick to update link positions with the new data
                    // This ensures D3's forceLink properly connects sources and targets
                    this.simulation.tick();
                    
                    // Manually update the visual positions immediately
                    if (this.linkSelection) {
                        this.linkSelection
                            .attr('x1', d => d.source.x)
                            .attr('y1', d => d.source.y)
                            .attr('x2', d => d.target.x)
                            .attr('y2', d => d.target.y);
                    }
                    
                    if (this.linkLabelSelection) {
                        this.linkLabelSelection
                            .attr('x', d => (d.source.x + d.target.x) / 2)
                            .attr('y', d => (d.source.y + d.target.y) / 2 - 5);
                    }
                    
                    // Keep simulation stopped - nodes won't move
                } else if (nodesChanged) {
                    // Nodes added/removed: use moderate restart
                    logger.debug('TopologyGraphComponent: Node structure changed, moderate restart');
                    this.simulation.alpha(0.3).restart();
                } else {
                    // Just links changed with same nodes
                    logger.debug('TopologyGraphComponent: Link structure changed, restarting simulation');
                    this.simulation.alpha(0.2).restart();
                }
            } else {
                // Property-only change: just update data, no restart needed
                // The simulation keeps running at its current alpha
                logger.debug('TopologyGraphComponent: Property-only update, continuing simulation');
                // Don't call restart() - let it continue naturally
            }
        }
    }

    addLegend(svgGroup) {
        // Remove existing legend to recreate with current mode
        svgGroup.select('.legend-group').remove();

        const legend = svgGroup.append('g')
            .attr('class', 'legend-group graph-element')
            .attr('transform', `translate(120, 120)`)  // Hidden by CSS opacity
            .style('opacity', '0');

        legend.append('rect')
            .attr('width', 420)
            .attr('height', 140)
            .attr('fill', 'rgba(0, 0, 0, 0.7)')
            .attr('rx', 8)
            .attr('stroke', 'rgba(255, 255, 255, 0.2)')
            .attr('stroke-width', 1);

        const nodeLegend = legend.append('g')
            .attr('transform', 'translate(20, 20)');
        
        nodeLegend.append('text')
            .text('Node Status:')
            .attr('x', 0)
            .attr('y', 0)
            .attr('font-size', '14px')
            .attr('font-weight', '600')
            .attr('fill', '#ecf0f1');

        const statuses = [
            { status: 'ACTIVE', color: '#10b981', y: 20 },
            { status: 'INACTIVE', color: '#f59e0b', y: 40 },
            { status: 'DEAD', color: '#ef4444', y: 60 }
        ];

        statuses.forEach(item => {
            nodeLegend.append('circle')
                .attr('r', 6)
                .attr('cx', 0)
                .attr('cy', item.y)
                .attr('fill', item.color);

            nodeLegend.append('text')
                .text(item.status)
                .attr('x', 15)
                .attr('y', item.y + 4)
                .attr('font-size', '12px')
                .attr('fill', '#ecf0f1');
        });

        const linkLegend = legend.append('g')
            .attr('transform', 'translate(150, 20)');

        linkLegend.append('text')
            .text('Link Latency:')
            .attr('x', 0)
            .attr('y', 0)
            .attr('font-size', '14px')
            .attr('font-weight', '600')
            .attr('fill', '#ecf0f1');

        const latencies = [
            { range: '≤30ms', color: '#10b981', y: 20 },
            { range: '31-50ms', color: '#f59e0b', y: 40 },
            { range: '>50ms', color: '#ef4444', y: 60 }
        ];

        latencies.forEach(item => {
            linkLegend.append('line')
                .attr('x1', 0)
                .attr('y1', item.y)
                .attr('x2', 20)
                .attr('y2', item.y)
                .attr('stroke', item.color)
                .attr('stroke-width', 2);

            linkLegend.append('text')
                .text(item.range)
                .attr('x', 25)
                .attr('y', item.y + 4)
                .attr('font-size', '12px')
                .attr('fill', '#ecf0f1');
        });

        const topologyLegend = legend.append('g')
            .attr('transform', 'translate(280, 20)');

        topologyLegend.append('text')
            .text('Topology:')
            .attr('x', 0)
            .attr('y', 0)
            .attr('font-size', '14px')
            .attr('font-weight', '600')
            .attr('fill', '#ecf0f1');

        // Primary node indicator
        const primaryBadge = topologyLegend.append('g')
            .attr('transform', 'translate(0, 20)');
        
        primaryBadge.append('circle')
            .attr('r', 6)
            .attr('fill', '#FFD700')
            .attr('stroke', '#FFF')
            .attr('stroke-width', 1);
        
        primaryBadge.append('text')
            .text('A')
            .attr('text-anchor', 'middle')
            .attr('dominant-baseline', 'central')
            .attr('font-size', '8px')
            .attr('font-weight', 'bold')
            .attr('fill', '#000');

        topologyLegend.append('text')
            .text('Primary Node')
            .attr('x', 15)
            .attr('y', 24)
            .attr('font-size', '12px')
            .attr('fill', '#ecf0f1');

        // Topology mode info
        const topologyMode = this.viewModel.get('topologyMode') || 'mesh';
        const modeText = topologyMode === 'mesh' ? 'Full Mesh' : 'Star Topology';
        const modeDesc = topologyMode === 'mesh' ? '(All to All)' : '(Center to Others)';
        
        topologyLegend.append('text')
            .text(modeText)
            .attr('x', 0)
            .attr('y', 50)
            .attr('font-size', '12px')
            .attr('fill', '#ecf0f1');

        topologyLegend.append('text')
            .text(modeDesc)
            .attr('x', 0)
            .attr('y', 65)
            .attr('font-size', '10px')
            .attr('fill', 'rgba(236, 240, 241, 0.7)');
    }

    getNodeRadius(status) {
        switch (status?.toUpperCase()) {
            case 'ACTIVE':
                return 10;
            case 'INACTIVE':
                return 8;
            case 'DEAD':
                return 6;
            default:
                return 8;
        }
    }

    getStatusIndicatorColor(status) {
        switch (status?.toUpperCase()) {
            case 'ACTIVE':
                return '#10b981';
            case 'INACTIVE':
                return '#f59e0b';
            case 'DEAD':
                return '#ef4444';
            default:
                return '#6b7280';
        }
    }

    getLinkColor(latency) {
        if (latency <= 50) return '#10b981';
        if (latency <= 100) return '#f59e0b';
        return '#ef4444';
    }

    getNodeColor(status) {
        switch (status?.toUpperCase()) {
            case 'ACTIVE':
                return '#10b981';
            case 'INACTIVE':
                return '#f59e0b';
            case 'DEAD':
                return '#ef4444';
            default:
                return '#6b7280';
        }
    }

    drag(simulation) {
        return d3.drag()
            .on('start', (event, d) => {
                // Set dragging flag to defer updates
                this.isDragging = true;
                logger.debug('TopologyGraphComponent: Drag started, updates deferred');
                
                if (!event.active && simulation && simulation.alphaTarget) {
                    simulation.alphaTarget(0.3).restart();
                }
                d.fx = d.x;
                d.fy = d.y;
            })
            .on('drag', (event, d) => {
                d.fx = event.x;
                d.fy = event.y;
            })
            .on('end', (event, d) => {
                if (!event.active && simulation && simulation.alphaTarget) {
                    simulation.alphaTarget(0);
                }
                
                // Save the final position before releasing
                const finalX = d.fx;
                const finalY = d.fy;
                
                // Store the dragged position to preserve it across updates
                this.draggedNodePositions.set(d.id, { x: finalX, y: finalY });
                logger.debug(`TopologyGraphComponent: Saved dragged position for ${d.id}: (${finalX}, ${finalY})`);
                
                // Update the node data in view model with the new position
                this.updateNodePositionInViewModel(d.id, finalX, finalY);
                
                d.fx = null;
                d.fy = null;
                
                // Clear dragging flag
                this.isDragging = false;
                logger.debug('TopologyGraphComponent: Drag ended');
                
                // Process any pending updates
                if (this.pendingUpdate) {
                    logger.debug('TopologyGraphComponent: Processing deferred update after drag');
                    // Use setTimeout to ensure drag event completes first
                    setTimeout(() => {
                        this.renderGraph();
                    }, 50);
                }
            });
    }

    updateNodePositionInViewModel(nodeId, x, y) {
        // Update the node position in the view model to persist the drag
        const nodes = this.viewModel.get('nodes');
        if (nodes) {
            const node = nodes.find(n => n.id === nodeId);
            if (node) {
                node.x = x;
                node.y = y;
                logger.debug(`TopologyGraphComponent: Updated node ${nodeId} position in view model`);
            }
        }
    }

    updateSelection(selectedNodeId) {
        // Update visual selection
        if (!this.svg || !this.isInitialized) {
            return;
        }
        
        this.svg.selectAll('.node').select('circle')
            .attr('stroke-width', d => d.id === selectedNodeId ? 4 : 2)
            .attr('stroke', d => d.id === selectedNodeId ? '#2196F3' : '#fff');
    }

    handleDiscoveryEvent(discoveryEvent) {
        if (!discoveryEvent || !discoveryEvent.nodeIp) {
            return;
        }
        
        // Emit discovery dots from the discovered node to all other nodes
        this.emitDiscoveryDots(discoveryEvent.nodeIp);
    }

    emitDiscoveryDots(sourceNodeIp) {
        if (!this.svg || !this.isInitialized) {
            return;
        }
        
        const nodes = this.viewModel.get('nodes') || [];
        const sourceNode = nodes.find(n => n.ip === sourceNodeIp);
        
        if (!sourceNode || !sourceNode.x || !sourceNode.y) {
            return;
        }
        
        // Get the main graph group (that has the transform applied)
        const mainGroup = this.svg.select('g');
        
        // Get or create animation group inside the main transformed group
        let animGroup = mainGroup.select('.discovery-animation-group');
        if (animGroup.empty()) {
            animGroup = mainGroup.append('g')
                .attr('class', 'discovery-animation-group')
                .style('pointer-events', 'none'); // Make entire animation group non-interactive
        }
        
        // Emit a dot to each other node
        nodes.forEach(targetNode => {
            if (targetNode.ip !== sourceNodeIp && targetNode.x && targetNode.y) {
                this.animateDiscoveryDot(animGroup, sourceNode, targetNode);
            }
        });
    }

    animateDiscoveryDot(animGroup, sourceNode, targetNode) {
        // Calculate spawn position outside the node boundary
        const nodeRadius = this.getNodeRadius(sourceNode.status);
        const spawnDistance = nodeRadius + 8; // Spawn 8px outside the node
        
        // Calculate direction from source to target
        const dx = targetNode.x - sourceNode.x;
        const dy = targetNode.y - sourceNode.y;
        const distance = Math.sqrt(dx * dx + dy * dy);
        
        // Normalize direction and spawn outside the node
        const normalizedX = dx / distance;
        const normalizedY = dy / distance;
        const spawnX = sourceNode.x + normalizedX * spawnDistance;
        const spawnY = sourceNode.y + normalizedY * spawnDistance;
        
        // Create a small circle outside the source node
        const dot = animGroup.append('circle')
            .attr('class', 'discovery-dot')
            .attr('r', 6)
            .attr('cx', spawnX)
            .attr('cy', spawnY)
            .attr('fill', '#FFD700')
            .attr('opacity', 1)
            .attr('stroke', '#FFF')
            .attr('stroke-width', 2)
            .style('pointer-events', 'none'); // Make dots non-interactive
        
        // Trigger response dot early (after 70% of the journey) - only if target node is active
        setTimeout(() => {
            if (targetNode.status && targetNode.status.toUpperCase() === 'ACTIVE') {
                this.animateResponseDot(animGroup, targetNode, sourceNode);
            }
        }, 1050); // 1500ms * 0.7 = 1050ms
        
        // Animate the dot to the target node
        dot.transition()
            .duration(1500)
            .ease(d3.easeCubicInOut)
            .attr('cx', targetNode.x)
            .attr('cy', targetNode.y)
            .attr('opacity', 0)
            .remove();
    }

    animateResponseDot(animGroup, sourceNode, targetNode) {
        // Calculate spawn position outside the target node boundary
        const nodeRadius = this.getNodeRadius(sourceNode.status);
        const spawnDistance = nodeRadius + 8; // Spawn 8px outside the node
        
        // Calculate direction from target back to original source
        const dx = targetNode.x - sourceNode.x;
        const dy = targetNode.y - sourceNode.y;
        const distance = Math.sqrt(dx * dx + dy * dy);
        
        // Normalize direction and spawn outside the node
        const normalizedX = dx / distance;
        const normalizedY = dy / distance;
        const spawnX = sourceNode.x + normalizedX * spawnDistance;
        const spawnY = sourceNode.y + normalizedY * spawnDistance;
        
        // Calculate target position outside the original source node
        const targetNodeRadius = this.getNodeRadius(targetNode.status);
        const targetDistance = targetNodeRadius + 8; // Stop 8px outside the target node
        
        // Calculate direction from source back to target
        const targetDx = sourceNode.x - targetNode.x;
        const targetDy = sourceNode.y - targetNode.y;
        const targetNormalizedX = targetDx / distance;
        const targetNormalizedY = targetDy / distance;
        const targetX = targetNode.x + targetNormalizedX * targetDistance;
        const targetY = targetNode.y + targetNormalizedY * targetDistance;
        
        // Create a response dot outside the target (now source) node
        const responseDot = animGroup.append('circle')
            .attr('class', 'discovery-dot-response')
            .attr('r', 5)
            .attr('cx', spawnX)
            .attr('cy', spawnY)
            .attr('fill', '#2196F3')
            .attr('opacity', 1)
            .attr('stroke', '#FFF')
            .attr('stroke-width', 2)
            .style('pointer-events', 'none'); // Make dots non-interactive
        
        // Animate back to outside the original source node
        responseDot.transition()
            .duration(1000)
            .ease(d3.easeCubicInOut)
            .attr('cx', targetX)
            .attr('cy', targetY)
            .attr('opacity', 0)
            .remove();
    }

    // NOTE: This method is deprecated and should not be used
    // The topology graph is now entirely websocket-driven
    // Refresh button was removed and all updates come from websocket events
    handleRefresh() {
        logger.warn('TopologyGraphComponent: handleRefresh called - this method is deprecated');
        logger.warn('TopologyGraphComponent: Topology updates should come from websocket events only');
        // No-op - do not make API calls
    }

    handleLoadingState(isLoading) {
        logger.debug('TopologyGraphComponent: handleLoadingState called with:', isLoading);
        const container = this.findElement('#topology-graph-container');
        
        if (isLoading) {
            // Only show loading state if there's no SVG already rendered
            // This prevents clearing the graph during updates
            const hasSVG = container.querySelector('svg');
            if (!hasSVG) {
                container.innerHTML = '<div class="loading"><div>Loading network topology...</div></div>';
            } else {
                logger.debug('TopologyGraphComponent: SVG exists, skipping loading state to preserve graph');
            }
        }
    }

    handleError() {
        const error = this.viewModel.get('error');
        if (error) {
            const container = this.findElement('#topology-graph-container');
            container.innerHTML = `<div class="error"><div>Error: ${error}</div></div>`;
        }
    }

    showNoData() {
        const container = this.findElement('#topology-graph-container');
        
        // Only show no-data state if there's no SVG already rendered
        // This prevents clearing the graph during transient states
        const hasSVG = container.querySelector('svg');
        if (!hasSVG) {
            container.innerHTML = '<div class="no-data"><div>No cluster members found</div></div>';
        } else {
            logger.debug('TopologyGraphComponent: SVG exists, keeping existing graph visible');
        }
    }

    showMemberCardOverlay(nodeData) {
        // Create overlay container if it doesn't exist
        let overlayContainer = document.getElementById('member-card-overlay');
        if (!overlayContainer) {
            overlayContainer = document.createElement('div');
            overlayContainer.id = 'member-card-overlay';
            overlayContainer.className = 'member-card-overlay';
            document.body.appendChild(overlayContainer);
        }

        // Create and show the overlay component
        if (!this.memberOverlayComponent) {
            const overlayVM = new ViewModel();
            this.memberOverlayComponent = new MemberCardOverlayComponent(overlayContainer, overlayVM, this.eventBus);
            this.memberOverlayComponent.mount();
        }

        // Convert node data to member data format
        const memberData = {
            ip: nodeData.ip,
            hostname: nodeData.hostname,
            status: this.normalizeStatus(nodeData.status),
            latency: nodeData.latency,
            labels: (nodeData.labels && typeof nodeData.labels === 'object') ? nodeData.labels : (nodeData.resources || {})
        };

        this.memberOverlayComponent.show(memberData);
    }

    // Normalize status from topology format to member card format
    normalizeStatus(status) {
        if (!status) return 'unknown';
        
        const normalized = status.toLowerCase();
        switch (normalized) {
            case 'active':
                return 'active';
            case 'inactive':
                return 'inactive';
            case 'dead':
                return 'offline';
            default:
                return 'unknown';
        }
    }

    // Override render method to display the graph
    render() {
        logger.debug('TopologyGraphComponent: render called');
        if (!this.isInitialized) {
            logger.debug('TopologyGraphComponent: Not initialized yet, skipping render');
            return;
        }
        const nodes = this.viewModel.get('nodes');
        const links = this.viewModel.get('links');
        const isLoading = this.viewModel.get('isLoading');
        
        if (nodes && nodes.length > 0) {
            logger.debug('TopologyGraphComponent: Rendering graph with data');
            this.renderGraph();
        } else if (isLoading) {
            logger.debug('TopologyGraphComponent: Loading, showing loading state');
            this.handleLoadingState(true);
        } else {
            logger.debug('TopologyGraphComponent: No data available, showing no data state');
            this.showNoData();
        }
    }

    unmount() {
        // Clean up resize listener
        if (this.resizeHandler) {
            window.removeEventListener('resize', this.resizeHandler);
            this.resizeHandler = null;
        }
        
        // Clear resize timeout
        if (this.resizeTimeout) {
            clearTimeout(this.resizeTimeout);
            this.resizeTimeout = null;
        }
        
        // Clear dragged node positions
        if (this.draggedNodePositions) {
            this.draggedNodePositions.clear();
        }
        
        // Call parent unmount
        super.unmount();
    }
}

// Minimal Member Card Overlay Component (kept in same file to avoid circular loads)
class MemberCardOverlayComponent extends Component {
    constructor(container, viewModel, eventBus) {
        super(container, viewModel, eventBus);
        this.isVisible = false;
        this.currentMember = null;
    }

    mount() {
        super.mount();
        this.setupEventListeners();
    }

    setupEventListeners() {
        // Close overlay when clicking outside or pressing escape
        this.addEventListener(this.container, 'click', (e) => {
            if (!this.isVisible) return;
            if (e.target === this.container) {
                this.hide();
            }
        });

        this.addEventListener(document, 'keydown', (e) => {
            if (e.key === 'Escape' && this.isVisible) {
                this.hide();
            }
        });
    }

    show(memberData) {
        this.currentMember = memberData;
        this.isVisible = true;
        
        const memberCardHTML = this.renderMemberCard(memberData);
        this.setHTML('', memberCardHTML);
        
        setTimeout(() => {
            this.container.classList.add('visible');
        }, 10);
        
        this.setupMemberCardInteractions();
    }

    hide() {
        this.isVisible = false;
        this.container.classList.remove('visible');
        this.currentMember = null;
    }

    renderMemberCard(member) {
        const statusClass = (member.status && member.status.toUpperCase() === 'ACTIVE') ? 'status-online' : 
                          (member.status && member.status.toUpperCase() === 'INACTIVE') ? 'status-inactive' : 'status-offline';
        const statusIcon = (member.status && member.status.toUpperCase() === 'ACTIVE') ? '🟢' : 
                          (member.status && member.status.toUpperCase() === 'INACTIVE') ? '🟠' : '🔴';
        
        return `
            <div class="member-overlay-content">
                <div class="member-overlay-header">
                    <div class="member-info">
                        <div class="member-row-1">
                            <div class="status-hostname-group">
                                <div class="member-status ${statusClass}">
                                    ${statusIcon}
                                </div>
                                <div class="member-hostname">${member.hostname || 'Unknown Device'}</div>
                            </div>
                            <div class="member-ip">${member.ip || 'No IP'}</div>
                            <div class="member-latency">
                                <span class="latency-label">Latency:</span>
                                <span class="latency-value">${member.latency ? member.latency + 'ms' : 'N/A'}</span>
                            </div>
                        </div>
                        <div class="member-labels" style="display: none;"></div>
                    </div>
                    <button class="member-overlay-close" aria-label="Close">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <path d="M18 6L6 18M6 6l12 12"/>
                        </svg>
                    </button>
                </div>
                
                <div class="member-overlay-body">
                    <div class="member-card expanded" data-member-ip="${member.ip}">
                        <div class="member-details">
                            <div class="loading-details">Loading detailed information...</div>
                        </div>
                    </div>
                </div>
            </div>
        `;
    }

    setupMemberCardInteractions() {
        const closeBtn = this.findElement('.member-overlay-close');
        if (closeBtn) {
            this.addEventListener(closeBtn, 'click', () => {
                this.hide();
            });
        }

        setTimeout(async () => {
            const memberCard = this.findElement('.member-card');
            if (memberCard) {
                const memberDetails = memberCard.querySelector('.member-details');
                const memberIp = memberCard.dataset.memberIp;
                await this.expandCard(memberCard, memberIp, memberDetails);
            }
        }, 100);
    }

    async expandCard(card, memberIp, memberDetails) {
        try {
            const nodeDetailsVM = new NodeDetailsViewModel();
            const nodeDetailsComponent = new NodeDetailsComponent(memberDetails, nodeDetailsVM, this.eventBus);
            await nodeDetailsVM.loadNodeDetails(memberIp);

            const nodeStatus = nodeDetailsVM.get('nodeStatus');
            if (nodeStatus && nodeStatus.labels) {
                const labelsContainer = document.querySelector('.member-overlay-header .member-labels');
                if (labelsContainer) {
                    labelsContainer.innerHTML = Object.entries(nodeStatus.labels)
                        .map(([key, value]) => `<span class="label-chip">${key}: ${value}</span>`)
                        .join('');
                    labelsContainer.style.display = 'block';
                }
            }
            nodeDetailsComponent.mount();
            card.classList.add('expanded');
        } catch (error) {
            logger.error('Failed to expand member card:', error);
            card.classList.add('expanded');
            const details = card.querySelector('.member-details');
            if (details) {
                details.innerHTML = '<div class="error">Failed to load node details</div>';
            }
        }
    }
}

window.TopologyGraphComponent = TopologyGraphComponent;
window.MemberCardOverlayComponent = MemberCardOverlayComponent; 