"""
Integrated pipeline dashboard for the Cluster4NPU UI application.

This module provides the main dashboard window that combines pipeline editing,
stage configuration, performance estimation, and dongle management in a unified
interface with a 3-panel layout.

Main Components:
    - IntegratedPipelineDashboard: Main dashboard window
    - Node template palette for pipeline design
    - Dynamic property editing panels
    - Performance estimation and hardware management
    - Pipeline save/load functionality

Usage:
    from cluster4npu_ui.ui.windows.dashboard import IntegratedPipelineDashboard
    
    dashboard = IntegratedPipelineDashboard()
    dashboard.show()
"""

import sys
import json
import os
from typing import Optional, Dict, Any, List

from PyQt5.QtWidgets import (
    QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QLineEdit, QPushButton, 
    QLabel, QSpinBox, QDoubleSpinBox, QComboBox, QListWidget, QCheckBox, 
    QSplitter, QAction, QScrollArea, QTabWidget, QTableWidget, QTableWidgetItem, 
    QHeaderView, QProgressBar, QGroupBox, QGridLayout, QFrame, QTextBrowser,
    QSizePolicy, QMessageBox, QFileDialog, QFormLayout, QToolBar, QStatusBar
)
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
from PyQt5.QtGui import QFont

try:
    from NodeGraphQt import NodeGraph
    NODEGRAPH_AVAILABLE = True
except ImportError:
    NODEGRAPH_AVAILABLE = False
    print("Warning: NodeGraphQt not available. Pipeline editor will be disabled.")

from cluster4npu_ui.config.theme import HARMONIOUS_THEME_STYLESHEET
from cluster4npu_ui.config.settings import get_settings
try:
    from cluster4npu_ui.core.nodes import (
        InputNode, ModelNode, PreprocessNode, PostprocessNode, OutputNode,
        NODE_TYPES, create_node_property_widget
    )
    ADVANCED_NODES_AVAILABLE = True
except ImportError:
    ADVANCED_NODES_AVAILABLE = False

# Use exact nodes that match original properties
from cluster4npu_ui.core.nodes.exact_nodes import (
    ExactInputNode, ExactModelNode, ExactPreprocessNode, 
    ExactPostprocessNode, ExactOutputNode, EXACT_NODE_TYPES
)

# Import pipeline analysis functions
try:
    from cluster4npu_ui.core.pipeline import get_stage_count, analyze_pipeline_stages, get_pipeline_summary
except ImportError:
    # Fallback functions if not available
    def get_stage_count(graph):
        return 0
    def analyze_pipeline_stages(graph):
        return {}
    def get_pipeline_summary(graph):
        return {'stage_count': 0, 'valid': True, 'error': '', 'total_nodes': 0, 'model_nodes': 0, 'input_nodes': 0, 'output_nodes': 0, 'preprocess_nodes': 0, 'postprocess_nodes': 0, 'stages': []}


class StageCountWidget(QWidget):
    """Widget to display stage count information in the pipeline editor."""
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self.stage_count = 0
        self.pipeline_valid = True
        self.pipeline_error = ""
        
        self.setup_ui()
        self.setFixedSize(120, 22)
    
    def setup_ui(self):
        """Setup the stage count widget UI."""
        layout = QHBoxLayout()
        layout.setContentsMargins(5, 2, 5, 2)
        
        # Stage count label only (compact version)
        self.stage_label = QLabel("Stages: 0")
        self.stage_label.setFont(QFont("Arial", 10, QFont.Bold))
        self.stage_label.setStyleSheet("color: #cdd6f4; font-weight: bold;")
        
        layout.addWidget(self.stage_label)
        self.setLayout(layout)
        
        # Style the widget for status bar - ensure it's visible
        self.setStyleSheet("""
            StageCountWidget {
                background-color: transparent;
                border: none;
            }
        """)
        
        # Ensure the widget is visible
        self.setVisible(True)
        self.stage_label.setVisible(True)
    
    def update_stage_count(self, count: int, valid: bool = True, error: str = ""):
        """Update the stage count display."""
        self.stage_count = count
        self.pipeline_valid = valid
        self.pipeline_error = error
        
        # Update stage count with status indication
        if not valid:
            self.stage_label.setText(f"Stages: {count}")
            self.stage_label.setStyleSheet("color: #f38ba8; font-weight: bold;")
        else:
            if count == 0:
                self.stage_label.setText("Stages: 0")
                self.stage_label.setStyleSheet("color: #f9e2af; font-weight: bold;")
            else:
                self.stage_label.setText(f"Stages: {count}")
                self.stage_label.setStyleSheet("color: #a6e3a1; font-weight: bold;")


class IntegratedPipelineDashboard(QMainWindow):
    """
    Integrated dashboard combining pipeline editor, stage configuration, and performance estimation.
    
    This is the main application window that provides a comprehensive interface for
    designing, configuring, and managing ML inference pipelines.
    """
    
    # Signals
    pipeline_modified = pyqtSignal()
    node_selected = pyqtSignal(object)
    pipeline_changed = pyqtSignal()
    stage_count_changed = pyqtSignal(int)
    
    def __init__(self, project_name: str = "", description: str = "", filename: Optional[str] = None):
        super().__init__()
        
        # Project information
        self.project_name = project_name or "Untitled Pipeline"
        self.description = description
        self.current_file = filename
        self.is_modified = False
        
        # Settings
        self.settings = get_settings()
        
        # Initialize UI components that will be created later
        self.props_instructions = None
        self.node_props_container = None
        self.node_props_layout = None
        self.fps_label = None
        self.latency_label = None
        self.memory_label = None
        self.suggestions_text = None
        self.dongles_list = None
        self.detected_devices = []  # Store detected device information
        self.stage_count_widget = None
        self.analysis_timer = None
        self.previous_stage_count = 0
        self.stats_label = None
        
        # Initialize node graph if available
        if NODEGRAPH_AVAILABLE:
            self.setup_node_graph()
        else:
            self.graph = None
        
        # Setup UI
        self.setup_integrated_ui()
        self.setup_menu()
        self.setup_shortcuts()
        self.setup_analysis_timer()
        
        # Apply styling and configure window
        self.apply_styling()
        self.update_window_title()
        self.setGeometry(50, 50, 1400, 900)
        
        # Connect signals
        self.pipeline_changed.connect(self.analyze_pipeline)
        
        # Initial analysis
        print("🚀 Pipeline Dashboard initialized")
        self.analyze_pipeline()
        
        # Set up a timer to hide UI elements after initialization
        self.ui_cleanup_timer = QTimer()
        self.ui_cleanup_timer.setSingleShot(True)
        self.ui_cleanup_timer.timeout.connect(self.cleanup_node_graph_ui)
        self.ui_cleanup_timer.start(1000)  # 1 second delay
    
    def setup_node_graph(self):
        """Initialize the node graph system."""
        try:
            self.graph = NodeGraph()
            
            # Configure NodeGraphQt to hide unwanted UI elements
            viewer = self.graph.viewer()
            if viewer:
                # Hide the logo/icon in bottom left corner
                if hasattr(viewer, 'set_logo_visible'):
                    viewer.set_logo_visible(False)
                elif hasattr(viewer, 'show_logo'):
                    viewer.show_logo(False)
                
                # Try to hide grid
                if hasattr(viewer, 'set_grid_mode'):
                    viewer.set_grid_mode(0)  # 0 = no grid
                elif hasattr(viewer, 'grid_mode'):
                    viewer.grid_mode = 0
                
                # Try to hide navigation widget/toolbar
                if hasattr(viewer, 'set_nav_widget_visible'):
                    viewer.set_nav_widget_visible(False)
                elif hasattr(viewer, 'navigation_widget'):
                    nav_widget = viewer.navigation_widget()
                    if nav_widget:
                        nav_widget.setVisible(False)
                
                # Try to hide any other UI elements
                if hasattr(viewer, 'set_minimap_visible'):
                    viewer.set_minimap_visible(False)
                
                # Hide menu bar if exists
                if hasattr(viewer, 'set_menu_bar_visible'):
                    viewer.set_menu_bar_visible(False)
                
                # Try to hide any toolbar elements
                widget = viewer.widget if hasattr(viewer, 'widget') else None
                if widget:
                    # Find and hide toolbar-like children
                    from PyQt5.QtWidgets import QToolBar, QFrame, QWidget
                    for child in widget.findChildren(QToolBar):
                        child.setVisible(False)
                    
                    # Look for other UI widgets that might be the horizontal bar
                    for child in widget.findChildren(QFrame):
                        # Check if this might be the navigation bar
                        if hasattr(child, 'objectName') and 'nav' in child.objectName().lower():
                            child.setVisible(False)
                        # Check size and position to identify the horizontal bar
                        elif hasattr(child, 'geometry'):
                            geom = child.geometry()
                            # If it's a horizontal bar at the bottom left
                            if geom.height() < 50 and geom.width() > 100:
                                child.setVisible(False)
                    
                    # Additional attempt to hide navigation elements
                    for child in widget.findChildren(QWidget):
                        if hasattr(child, 'objectName'):
                            obj_name = child.objectName().lower()
                            if any(keyword in obj_name for keyword in ['nav', 'toolbar', 'control', 'zoom']):
                                child.setVisible(False)
            
            # Use exact nodes that match original properties
            nodes_to_register = [
                ExactInputNode, ExactModelNode, ExactPreprocessNode, 
                ExactPostprocessNode, ExactOutputNode
            ]
            
            print("Registering nodes with NodeGraphQt...")
            for node_class in nodes_to_register:
                try:
                    self.graph.register_node(node_class)
                    print(f"✓ Registered {node_class.__name__} with identifier {node_class.__identifier__}")
                except Exception as e:
                    print(f"✗ Failed to register {node_class.__name__}: {e}")
            
            # Connect signals
            self.graph.node_created.connect(self.mark_modified)
            self.graph.nodes_deleted.connect(self.mark_modified)
            self.graph.node_selection_changed.connect(self.on_node_selection_changed)
            
            # Connect pipeline analysis signals
            self.graph.node_created.connect(self.schedule_analysis)
            self.graph.nodes_deleted.connect(self.schedule_analysis)
            if hasattr(self.graph, 'connection_changed'):
                self.graph.connection_changed.connect(self.schedule_analysis)
            
            if hasattr(self.graph, 'property_changed'):
                self.graph.property_changed.connect(self.mark_modified)
            
            print("Node graph setup completed successfully")
            
        except Exception as e:
            print(f"Error setting up node graph: {e}")
            import traceback
            traceback.print_exc()
            self.graph = None
    
    def cleanup_node_graph_ui(self):
        """Clean up NodeGraphQt UI elements after initialization."""
        if not self.graph:
            return
            
        try:
            viewer = self.graph.viewer()
            if viewer:
                widget = viewer.widget if hasattr(viewer, 'widget') else None
                if widget:
                    print("🧹 Cleaning up NodeGraphQt UI elements...")
                    
                    # More aggressive cleanup - hide all small widgets at bottom
                    from PyQt5.QtWidgets import QWidget, QFrame, QLabel, QPushButton
                    from PyQt5.QtCore import QRect
                    
                    for child in widget.findChildren(QWidget):
                        if hasattr(child, 'geometry'):
                            geom = child.geometry()
                            parent_geom = widget.geometry()
                            
                            # Check if it's a small widget at the bottom left
                            if (geom.height() < 100 and 
                                geom.width() < 200 and 
                                geom.y() > parent_geom.height() - 100 and
                                geom.x() < 200):
                                print(f"🗑️  Hiding bottom-left widget: {child.__class__.__name__}")
                                child.setVisible(False)
                    
                    # Also try to hide by CSS styling
                    try:
                        widget.setStyleSheet(widget.styleSheet() + """
                            QWidget[objectName*="nav"] { display: none; }
                            QWidget[objectName*="toolbar"] { display: none; }
                            QWidget[objectName*="control"] { display: none; }
                            QFrame[objectName*="zoom"] { display: none; }
                        """)
                    except:
                        pass
                        
        except Exception as e:
            print(f"Error cleaning up NodeGraphQt UI: {e}")
    
    def setup_integrated_ui(self):
        """Setup the integrated UI with node templates, pipeline editor and configuration panels."""
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # Main layout with status bar at bottom
        main_layout = QVBoxLayout(central_widget)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.setSpacing(0)
        
        # Main horizontal splitter with 3 panels
        main_splitter = QSplitter(Qt.Horizontal)
        
        # Left side: Node Template Panel (25% width)
        left_panel = self.create_node_template_panel()
        left_panel.setMinimumWidth(250)
        left_panel.setMaximumWidth(350)
        
        # Middle: Pipeline Editor (50% width) - without its own status bar
        middle_panel = self.create_pipeline_editor_panel()
        
        # Right side: Configuration panels (25% width)
        right_panel = self.create_configuration_panel()
        right_panel.setMinimumWidth(300)
        right_panel.setMaximumWidth(400)
        
        # Add widgets to splitter
        main_splitter.addWidget(left_panel)
        main_splitter.addWidget(middle_panel)
        main_splitter.addWidget(right_panel)
        main_splitter.setSizes([300, 700, 400])  # 25-50-25 split
        
        # Add splitter to main layout
        main_layout.addWidget(main_splitter)
        
        # Add global status bar at the bottom
        self.global_status_bar = self.create_status_bar_widget()
        main_layout.addWidget(self.global_status_bar)
    
    def create_node_template_panel(self) -> QWidget:
        """Create left panel with node templates."""
        panel = QWidget()
        layout = QVBoxLayout(panel)
        layout.setContentsMargins(10, 10, 10, 10)
        layout.setSpacing(10)
        
        # Header
        header = QLabel("Node Templates")
        header.setStyleSheet("color: #f9e2af; font-size: 16px; font-weight: bold; padding: 10px;")
        layout.addWidget(header)
        
        # Node template buttons - use exact nodes matching original
        nodes_info = [
            ("Input Node", "Data input source", ExactInputNode),
            ("Model Node", "AI inference model", ExactModelNode), 
            ("Preprocess Node", "Data preprocessing", ExactPreprocessNode),
            ("Postprocess Node", "Output processing", ExactPostprocessNode),
            ("Output Node", "Final output", ExactOutputNode)
        ]
        
        for name, description, node_class in nodes_info:
            # Create container for each node type
            node_container = QFrame()
            node_container.setStyleSheet("""
                QFrame {
                    background-color: #313244;
                    border: 2px solid #45475a;
                    border-radius: 8px;
                    padding: 5px;
                }
                QFrame:hover {
                    border-color: #89b4fa;
                    background-color: #383a59;
                }
            """)
            
            container_layout = QVBoxLayout(node_container)
            container_layout.setContentsMargins(8, 8, 8, 8)
            container_layout.setSpacing(4)
            
            # Node name
            name_label = QLabel(name)
            name_label.setStyleSheet("color: #cdd6f4; font-weight: bold; font-size: 12px;")
            container_layout.addWidget(name_label)
            
            # Description
            desc_label = QLabel(description)
            desc_label.setStyleSheet("color: #a6adc8; font-size: 10px;")
            desc_label.setWordWrap(True)
            container_layout.addWidget(desc_label)
            
            # Add button
            add_btn = QPushButton("+ Add")
            add_btn.setStyleSheet("""
                QPushButton {
                    background-color: #89b4fa;
                    color: #1e1e2e;
                    border: none;
                    padding: 4px 8px;
                    border-radius: 4px;
                    font-size: 10px;
                    font-weight: bold;
                }
                QPushButton:hover {
                    background-color: #a6c8ff;
                }
                QPushButton:pressed {
                    background-color: #7287fd;
                }
            """)
            add_btn.clicked.connect(lambda checked, nc=node_class: self.add_node_to_graph(nc))
            container_layout.addWidget(add_btn)
            
            layout.addWidget(node_container)
        
        # Pipeline Operations Section
        operations_label = QLabel("Pipeline Operations")
        operations_label.setStyleSheet("color: #f9e2af; font-size: 14px; font-weight: bold; padding: 10px;")
        layout.addWidget(operations_label)
        
        # Create operation buttons
        operations = [
            ("Validate Pipeline", self.validate_pipeline),
            ("Clear Pipeline", self.clear_pipeline),
        ]
        
        for name, handler in operations:
            btn = QPushButton(name)
            btn.setStyleSheet("""
                QPushButton {
                    background-color: #45475a;
                    color: #cdd6f4;
                    border: 1px solid #585b70;
                    border-radius: 6px;
                    padding: 8px 12px;
                    font-size: 11px;
                    font-weight: bold;
                    margin: 2px;
                }
                QPushButton:hover {
                    background-color: #585b70;
                    border-color: #89b4fa;
                }
                QPushButton:pressed {
                    background-color: #313244;
                }
            """)
            btn.clicked.connect(handler)
            layout.addWidget(btn)
        
        # Add stretch to push everything to top
        layout.addStretch()
        
        # Instructions
        instructions = QLabel("Click 'Add' to insert nodes into the pipeline editor")
        instructions.setStyleSheet("""
            color: #f9e2af; 
            font-size: 10px; 
            padding: 10px;
            background-color: #313244;
            border-radius: 6px;
            border-left: 3px solid #89b4fa;
        """)
        instructions.setWordWrap(True)
        layout.addWidget(instructions)
        
        return panel
    
    def create_pipeline_editor_panel(self) -> QWidget:
        """Create the middle panel with pipeline editor."""
        panel = QWidget()
        layout = QVBoxLayout(panel)
        layout.setContentsMargins(5, 5, 5, 5)
        
        # Header
        header = QLabel("Pipeline Editor")
        header.setStyleSheet("color: #f9e2af; font-size: 16px; font-weight: bold; padding: 10px;")
        layout.addWidget(header)
        
        if self.graph and NODEGRAPH_AVAILABLE:
            # Add the node graph widget directly
            graph_widget = self.graph.widget
            graph_widget.setMinimumHeight(400)
            layout.addWidget(graph_widget)
        else:
            # Fallback: show placeholder
            placeholder = QLabel("Pipeline Editor\n(NodeGraphQt not available)")
            placeholder.setStyleSheet("""
                color: #6c7086; 
                font-size: 14px; 
                padding: 40px;
                background-color: #313244;
                border-radius: 8px;
                border: 2px dashed #45475a;
            """)
            placeholder.setAlignment(Qt.AlignCenter)
            layout.addWidget(placeholder)
        
        return panel
    
    def create_pipeline_toolbar(self) -> QToolBar:
        """Create toolbar for pipeline operations."""
        toolbar = QToolBar("Pipeline Operations")
        toolbar.setStyleSheet("""
            QToolBar {
                background-color: #313244;
                border: 1px solid #45475a;
                spacing: 5px;
                padding: 5px;
            }
            QToolBar QAction {
                padding: 5px 10px;
                margin: 2px;
                border: 1px solid #45475a;
                border-radius: 3px;
                background-color: #45475a;
                color: #cdd6f4;
            }
            QToolBar QAction:hover {
                background-color: #585b70;
            }
        """)
        
        # Add nodes actions
        add_input_action = QAction("Add Input", self)
        add_input_action.triggered.connect(lambda: self.add_node_to_graph(ExactInputNode))
        toolbar.addAction(add_input_action)
        
        add_model_action = QAction("Add Model", self)
        add_model_action.triggered.connect(lambda: self.add_node_to_graph(ExactModelNode))
        toolbar.addAction(add_model_action)
        
        add_preprocess_action = QAction("Add Preprocess", self)
        add_preprocess_action.triggered.connect(lambda: self.add_node_to_graph(ExactPreprocessNode))
        toolbar.addAction(add_preprocess_action)
        
        add_postprocess_action = QAction("Add Postprocess", self)
        add_postprocess_action.triggered.connect(lambda: self.add_node_to_graph(ExactPostprocessNode))
        toolbar.addAction(add_postprocess_action)
        
        add_output_action = QAction("Add Output", self)
        add_output_action.triggered.connect(lambda: self.add_node_to_graph(ExactOutputNode))
        toolbar.addAction(add_output_action)
        
        toolbar.addSeparator()
        
        # Pipeline actions
        validate_action = QAction("Validate Pipeline", self)
        validate_action.triggered.connect(self.validate_pipeline)
        toolbar.addAction(validate_action)
        
        clear_action = QAction("Clear Pipeline", self)
        clear_action.triggered.connect(self.clear_pipeline)
        toolbar.addAction(clear_action)
        
        toolbar.addSeparator()
        
        # Deploy action
        deploy_action = QAction("Deploy Pipeline", self)
        deploy_action.setToolTip("Convert pipeline to executable format and deploy to dongles")
        deploy_action.triggered.connect(self.deploy_pipeline)
        deploy_action.setStyleSheet("""
            QAction {
                background-color: #a6e3a1;
                color: #1e1e2e;
                font-weight: bold;
            }
            QAction:hover {
                background-color: #94d2a3;
            }
        """)
        toolbar.addAction(deploy_action)
        
        return toolbar
    
    def setup_analysis_timer(self):
        """Setup timer for pipeline analysis."""
        self.analysis_timer = QTimer()
        self.analysis_timer.setSingleShot(True)
        self.analysis_timer.timeout.connect(self.analyze_pipeline)
        self.analysis_timer.setInterval(500)  # 500ms delay
    
    def schedule_analysis(self):
        """Schedule pipeline analysis after a delay."""
        if self.analysis_timer:
            self.analysis_timer.start()
    
    def analyze_pipeline(self):
        """Analyze the current pipeline and update stage count."""
        if not self.graph:
            return
        
        try:
            # Get pipeline summary
            summary = get_pipeline_summary(self.graph)
            current_stage_count = summary['stage_count']
            
            # Print detailed pipeline analysis
            self.print_pipeline_analysis(summary, current_stage_count)
            
            # Update stage count widget
            if self.stage_count_widget:
                print(f"🔄 Updating stage count widget: {current_stage_count} stages")
                self.stage_count_widget.update_stage_count(
                    current_stage_count, 
                    summary['valid'], 
                    summary.get('error', '')
                )
            
            # Update statistics label
            if hasattr(self, 'stats_label') and self.stats_label:
                total_nodes = summary['total_nodes']
                # Count connections more accurately
                connection_count = 0
                if self.graph:
                    for node in self.graph.all_nodes():
                        try:
                            if hasattr(node, 'output_ports'):
                                for output_port in node.output_ports():
                                    if hasattr(output_port, 'connected_ports'):
                                        connection_count += len(output_port.connected_ports())
                            elif hasattr(node, 'outputs'):
                                for output in node.outputs():
                                    if hasattr(output, 'connected_ports'):
                                        connection_count += len(output.connected_ports())
                                    elif hasattr(output, 'connected_inputs'):
                                        connection_count += len(output.connected_inputs())
                        except Exception:
                            # If there's any error accessing connections, skip this node
                            continue
                
                self.stats_label.setText(f"Nodes: {total_nodes} | Connections: {connection_count}")
            
            # Update info panel (if it exists)
            if hasattr(self, 'info_text') and self.info_text:
                self.update_info_panel(summary)
            
            # Update previous count for next comparison
            self.previous_stage_count = current_stage_count
            
            # Emit signal
            self.stage_count_changed.emit(current_stage_count)
            
        except Exception as e:
            print(f"Pipeline analysis error: {str(e)}")
            if self.stage_count_widget:
                self.stage_count_widget.update_stage_count(0, False, f"Analysis error: {str(e)}")
    
    def print_pipeline_analysis(self, summary, current_stage_count):
        """Print detailed pipeline analysis to terminal."""
        # Check if stage count changed
        if current_stage_count != self.previous_stage_count:
            if self.previous_stage_count == 0 and current_stage_count > 0:
                print(f"Initial stage count: {current_stage_count}")
            elif current_stage_count != self.previous_stage_count:
                change = current_stage_count - self.previous_stage_count
                if change > 0:
                    print(f"Stage count increased: {self.previous_stage_count} → {current_stage_count} (+{change})")
                else:
                    print(f"Stage count decreased: {self.previous_stage_count} → {current_stage_count} ({change})")
        
        # Always print current pipeline status for clarity
        print(f"Current Pipeline Status:")
        print(f"   • Stages: {current_stage_count}")
        print(f"   • Total Nodes: {summary['total_nodes']}")
        print(f"   • Model Nodes: {summary['model_nodes']}")
        print(f"   • Input Nodes: {summary['input_nodes']}")
        print(f"   • Output Nodes: {summary['output_nodes']}")
        print(f"   • Preprocess Nodes: {summary['preprocess_nodes']}")
        print(f"   • Postprocess Nodes: {summary['postprocess_nodes']}")
        print(f"   • Valid: {'V' if summary['valid'] else 'X'}")
        
        if not summary['valid'] and summary.get('error'):
            print(f"   • Error: {summary['error']}")
        
        # Print stage details if available
        if summary.get('stages') and len(summary['stages']) > 0:
            print(f"Stage Details:")
            for i, stage in enumerate(summary['stages'], 1):
                model_name = stage['model_config'].get('node_name', 'Unknown Model')
                preprocess_count = len(stage['preprocess_configs'])
                postprocess_count = len(stage['postprocess_configs'])
                
                stage_info = f"   Stage {i}: {model_name}"
                if preprocess_count > 0:
                    stage_info += f" (with {preprocess_count} preprocess)"
                if postprocess_count > 0:
                    stage_info += f" (with {postprocess_count} postprocess)"
                
                print(stage_info)
        elif current_stage_count > 0:
            print(f"{current_stage_count} stage(s) detected but details not available")
        
        print("─" * 50)  # Separator line
    
    def update_info_panel(self, summary):
        """Update the pipeline info panel with analysis results."""
        # This method is kept for compatibility but no longer used
        # since we removed the separate info panel
        pass
    
    def clear_pipeline(self):
        """Clear the entire pipeline."""
        if self.graph:
            print("Clearing entire pipeline...")
            self.graph.clear_session()
            self.schedule_analysis()
    
    def create_configuration_panel(self) -> QWidget:
        """Create the right panel with configuration tabs."""
        panel = QWidget()
        layout = QVBoxLayout(panel)
        layout.setContentsMargins(5, 5, 5, 5)
        layout.setSpacing(10)
        
        # Create tabs for different configuration sections
        config_tabs = QTabWidget()
        config_tabs.setStyleSheet("""
            QTabWidget::pane {
                border: 2px solid #45475a;
                border-radius: 8px;
                background-color: #313244;
            }
            QTabWidget::tab-bar {
                alignment: center;
            }
            QTabBar::tab {
                background-color: #45475a;
                color: #cdd6f4;
                padding: 6px 12px;
                margin: 1px;
                border-radius: 4px;
                font-size: 11px;
            }
            QTabBar::tab:selected {
                background-color: #89b4fa;
                color: #1e1e2e;
                font-weight: bold;
            }
            QTabBar::tab:hover {
                background-color: #585b70;
            }
        """)
        
        # Add tabs
        config_tabs.addTab(self.create_node_properties_panel(), "Properties")
        config_tabs.addTab(self.create_performance_panel(), "Performance")
        config_tabs.addTab(self.create_dongle_panel(), "Dongles")
        
        layout.addWidget(config_tabs)
        return panel
    
    def create_node_properties_panel(self) -> QWidget:
        """Create node properties editing panel."""
        widget = QScrollArea()
        content = QWidget()
        layout = QVBoxLayout(content)
        
        # Header
        header = QLabel("Node Properties")
        header.setStyleSheet("color: #f9e2af; font-size: 14px; font-weight: bold; padding: 5px;")
        layout.addWidget(header)
        
        # Instructions when no node selected
        self.props_instructions = QLabel("Select a node in the pipeline editor to view and edit its properties")
        self.props_instructions.setStyleSheet("""
            color: #a6adc8; 
            font-size: 12px; 
            padding: 20px;
            background-color: #313244;
            border-radius: 8px;
            border: 2px dashed #45475a;
        """)
        self.props_instructions.setWordWrap(True)
        self.props_instructions.setAlignment(Qt.AlignCenter)
        layout.addWidget(self.props_instructions)
        
        # Container for dynamic properties
        self.node_props_container = QWidget()
        self.node_props_layout = QVBoxLayout(self.node_props_container)
        layout.addWidget(self.node_props_container)
        
        # Initially hide the container
        self.node_props_container.setVisible(False)
        
        layout.addStretch()
        widget.setWidget(content)
        widget.setWidgetResizable(True)
        
        return widget
    
    def create_status_bar_widget(self) -> QWidget:
        """Create a global status bar widget for pipeline information."""
        status_widget = QWidget()
        status_widget.setFixedHeight(28)
        status_widget.setStyleSheet("""
            QWidget {
                background-color: #1e1e2e;
                border-top: 1px solid #45475a;
                margin: 0px;
                padding: 0px;
            }
        """)
        
        layout = QHBoxLayout(status_widget)
        layout.setContentsMargins(15, 3, 15, 3)
        layout.setSpacing(20)
        
        # Left side: Stage count display
        self.stage_count_widget = StageCountWidget()
        self.stage_count_widget.setFixedSize(120, 22)
        layout.addWidget(self.stage_count_widget)
        
        # Center spacer
        layout.addStretch()
        
        # Right side: Pipeline statistics
        self.stats_label = QLabel("Nodes: 0 | Connections: 0")
        self.stats_label.setStyleSheet("color: #a6adc8; font-size: 10px;")
        layout.addWidget(self.stats_label)
        
        return status_widget
    
    def create_performance_panel(self) -> QWidget:
        """Create performance estimation panel."""
        widget = QScrollArea()
        content = QWidget()
        layout = QVBoxLayout(content)
        
        # Header
        header = QLabel("Performance Estimation")
        header.setStyleSheet("color: #f9e2af; font-size: 14px; font-weight: bold; padding: 5px;")
        layout.addWidget(header)
        
        # Performance metrics
        metrics_group = QGroupBox("Estimated Metrics")
        metrics_layout = QFormLayout(metrics_group)
        
        self.fps_label = QLabel("-- FPS")
        self.latency_label = QLabel("-- ms")
        self.memory_label = QLabel("-- MB")
        
        metrics_layout.addRow("Throughput:", self.fps_label)
        metrics_layout.addRow("Latency:", self.latency_label)
        metrics_layout.addRow("Memory Usage:", self.memory_label)
        
        layout.addWidget(metrics_group)
        
        # Suggestions
        suggestions_group = QGroupBox("Optimization Suggestions")
        suggestions_layout = QVBoxLayout(suggestions_group)
        
        self.suggestions_text = QTextBrowser()
        self.suggestions_text.setMaximumHeight(150)
        self.suggestions_text.setPlainText("Connect nodes to see performance analysis and optimization suggestions.")
        suggestions_layout.addWidget(self.suggestions_text)
        
        layout.addWidget(suggestions_group)
        
        # Deploy section
        deploy_group = QGroupBox("Pipeline Deployment")
        deploy_layout = QVBoxLayout(deploy_group)
        
        # Deploy button
        self.deploy_button = QPushButton("Deploy Pipeline")
        self.deploy_button.setToolTip("Convert pipeline to executable format and deploy to dongles")
        self.deploy_button.clicked.connect(self.deploy_pipeline)
        self.deploy_button.setStyleSheet("""
            QPushButton {
                background-color: #a6e3a1;
                color: #1e1e2e;
                border: 2px solid #a6e3a1;
                border-radius: 8px;
                padding: 12px 24px;
                font-weight: bold;
                font-size: 14px;
                min-height: 20px;
            }
            QPushButton:hover {
                background-color: #94d2a3;
                border-color: #94d2a3;
            }
            QPushButton:pressed {
                background-color: #7dc4b0;
                border-color: #7dc4b0;
            }
            QPushButton:disabled {
                background-color: #6c7086;
                color: #45475a;
                border-color: #6c7086;
            }
        """)
        deploy_layout.addWidget(self.deploy_button)
        
        # Deployment status
        self.deployment_status = QLabel("Ready to deploy")
        self.deployment_status.setStyleSheet("color: #a6adc8; font-size: 11px; margin-top: 5px;")
        self.deployment_status.setAlignment(Qt.AlignCenter)
        deploy_layout.addWidget(self.deployment_status)
        
        layout.addWidget(deploy_group)
        
        layout.addStretch()
        widget.setWidget(content)
        widget.setWidgetResizable(True)
        
        return widget
    
    def create_dongle_panel(self) -> QWidget:
        """Create dongle management panel."""
        widget = QScrollArea()
        content = QWidget()
        layout = QVBoxLayout(content)
        
        # Header
        header = QLabel("Dongle Management")
        header.setStyleSheet("color: #f9e2af; font-size: 14px; font-weight: bold; padding: 5px;")
        layout.addWidget(header)
        
        # Detect dongles button
        detect_btn = QPushButton("Detect Dongles")
        detect_btn.clicked.connect(self.detect_dongles)
        layout.addWidget(detect_btn)
        
        # Dongles list
        self.dongles_list = QListWidget()
        self.dongles_list.addItem("No dongles detected. Click 'Detect Dongles' to scan.")
        layout.addWidget(self.dongles_list)
        
        layout.addStretch()
        widget.setWidget(content)
        widget.setWidgetResizable(True)
        
        return widget
    
    def setup_menu(self):
        """Setup the menu bar."""
        menubar = self.menuBar()
        
        # File menu
        file_menu = menubar.addMenu('&File')
        
        # New pipeline
        new_action = QAction('&New Pipeline', self)
        new_action.setShortcut('Ctrl+N')
        new_action.triggered.connect(self.new_pipeline)
        file_menu.addAction(new_action)
        
        # Open pipeline
        open_action = QAction('&Open Pipeline...', self)
        open_action.setShortcut('Ctrl+O')
        open_action.triggered.connect(self.open_pipeline)
        file_menu.addAction(open_action)
        
        file_menu.addSeparator()
        
        # Save pipeline
        save_action = QAction('&Save Pipeline', self)
        save_action.setShortcut('Ctrl+S')
        save_action.triggered.connect(self.save_pipeline)
        file_menu.addAction(save_action)
        
        # Save As
        save_as_action = QAction('Save &As...', self)
        save_as_action.setShortcut('Ctrl+Shift+S')
        save_as_action.triggered.connect(self.save_pipeline_as)
        file_menu.addAction(save_as_action)
        
        file_menu.addSeparator()
        
        # Export
        export_action = QAction('&Export Configuration...', self)
        export_action.triggered.connect(self.export_configuration)
        file_menu.addAction(export_action)
        
        # Pipeline menu
        pipeline_menu = menubar.addMenu('&Pipeline')
        
        # Validate pipeline
        validate_action = QAction('&Validate Pipeline', self)
        validate_action.triggered.connect(self.validate_pipeline)
        pipeline_menu.addAction(validate_action)
        
        # Performance estimation
        perf_action = QAction('&Performance Analysis', self)
        perf_action.triggered.connect(self.update_performance_estimation)
        pipeline_menu.addAction(perf_action)
    
    def setup_shortcuts(self):
        """Setup keyboard shortcuts."""
        # Delete shortcut
        self.delete_shortcut = QAction("Delete", self)
        self.delete_shortcut.setShortcut('Delete')
        self.delete_shortcut.triggered.connect(self.delete_selected_nodes)
        self.addAction(self.delete_shortcut)
    
    def apply_styling(self):
        """Apply the application stylesheet."""
        self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET)
    
    # Event handlers and utility methods
    
    def add_node_to_graph(self, node_class):
        """Add a new node to the graph."""
        if not self.graph:
            QMessageBox.warning(self, "Node Graph Not Available", 
                              "NodeGraphQt is not available. Cannot add nodes.")
            return
        
        try:
            print(f"Attempting to create node with identifier: {node_class.__identifier__}")
            
            # Try different identifier formats that NodeGraphQt might use
            identifiers_to_try = [
                node_class.__identifier__,  # Original identifier
                f"{node_class.__identifier__}.{node_class.__name__}",  # Full format
                node_class.__name__,  # Just class name
            ]
            
            node = None
            for identifier in identifiers_to_try:
                try:
                    print(f"Trying identifier: {identifier}")
                    node = self.graph.create_node(identifier)
                    print(f"Success with identifier: {identifier}")
                    break
                except Exception as e:
                    print(f"Failed with {identifier}: {e}")
                    continue
            
            if not node:
                raise Exception("Could not create node with any identifier format")
            
            # Position the node with some randomization to avoid overlap
            import random
            x_pos = random.randint(50, 300)
            y_pos = random.randint(50, 300)
            node.set_pos(x_pos, y_pos)
            
            print(f"✓ Successfully created node: {node.name()}")
            self.mark_modified()
            
        except Exception as e:
            error_msg = f"Failed to create node: {e}"
            print(f"✗ {error_msg}")
            import traceback
            traceback.print_exc()
            
            # Show user-friendly error
            QMessageBox.critical(self, "Node Creation Error", 
                               f"Could not create {node_class.NODE_NAME}.\n\n"
                               f"Error: {e}\n\n"
                               f"This might be due to:\n"
                               f"• Node not properly registered\n"
                               f"• NodeGraphQt compatibility issue\n"
                               f"• Missing dependencies")
    
    def on_node_selection_changed(self):
        """Handle node selection changes."""
        if not self.graph:
            return
        
        selected_nodes = self.graph.selected_nodes()
        if selected_nodes:
            self.update_node_properties_panel(selected_nodes[0])
            self.node_selected.emit(selected_nodes[0])
        else:
            self.clear_node_properties_panel()
    
    def update_node_properties_panel(self, node):
        """Update the properties panel for the selected node."""
        if not self.node_props_container:
            return
        
        # Clear existing properties
        self.clear_node_properties_panel()
        
        # Show the container and hide instructions
        self.node_props_container.setVisible(True)
        self.props_instructions.setVisible(False)
        
        # Create property form
        form_widget = QWidget()
        form_layout = QFormLayout(form_widget)
        
        # Node info
        info_label = QLabel(f"Editing: {node.name()}")
        info_label.setStyleSheet("color: #89b4fa; font-weight: bold; margin-bottom: 10px;")
        form_layout.addRow(info_label)
        
        # Get node properties - try different methods
        try:
            properties = {}
            
            # Method 1: Try custom properties (for enhanced nodes)
            if hasattr(node, 'get_business_properties'):
                properties = node.get_business_properties()
            
            # Method 1.5: Try ExactNode properties (with _property_options)
            elif hasattr(node, '_property_options') and node._property_options:
                properties = {}
                for prop_name in node._property_options.keys():
                    if hasattr(node, 'get_property'):
                        try:
                            properties[prop_name] = node.get_property(prop_name)
                        except:
                            # If property doesn't exist, use a default value
                            properties[prop_name] = None
            
            # Method 2: Try standard NodeGraphQt properties
            elif hasattr(node, 'properties'):
                all_props = node.properties()
                # Filter out system properties, keep user properties
                for key, value in all_props.items():
                    if not key.startswith('_') and key not in ['name', 'selected', 'disabled', 'custom']:
                        properties[key] = value
            
            # Method 3: Use exact original properties based on node type
            else:
                node_type = node.__class__.__name__
                if 'Input' in node_type:
                    # Exact InputNode properties from original
                    properties = {
                        'source_type': node.get_property('source_type') if hasattr(node, 'get_property') else 'Camera',
                        'device_id': node.get_property('device_id') if hasattr(node, 'get_property') else 0,
                        'source_path': node.get_property('source_path') if hasattr(node, 'get_property') else '',
                        'resolution': node.get_property('resolution') if hasattr(node, 'get_property') else '1920x1080',
                        'fps': node.get_property('fps') if hasattr(node, 'get_property') else 30
                    }
                elif 'Model' in node_type:
                    # Exact ModelNode properties from original
                    properties = {
                        'model_path': node.get_property('model_path') if hasattr(node, 'get_property') else '',
                        'scpu_fw_path': node.get_property('scpu_fw_path') if hasattr(node, 'get_property') else '',
                        'ncpu_fw_path': node.get_property('ncpu_fw_path') if hasattr(node, 'get_property') else '',
                        'dongle_series': node.get_property('dongle_series') if hasattr(node, 'get_property') else '520',
                        'num_dongles': node.get_property('num_dongles') if hasattr(node, 'get_property') else 1,
                        'port_id': node.get_property('port_id') if hasattr(node, 'get_property') else '',
                        'upload_fw': node.get_property('upload_fw') if hasattr(node, 'get_property') else True
                    }
                elif 'Preprocess' in node_type:
                    # Exact PreprocessNode properties from original
                    properties = {
                        'resize_width': node.get_property('resize_width') if hasattr(node, 'get_property') else 640,
                        'resize_height': node.get_property('resize_height') if hasattr(node, 'get_property') else 480,
                        'normalize': node.get_property('normalize') if hasattr(node, 'get_property') else True,
                        'crop_enabled': node.get_property('crop_enabled') if hasattr(node, 'get_property') else False,
                        'operations': node.get_property('operations') if hasattr(node, 'get_property') else 'resize,normalize'
                    }
                elif 'Postprocess' in node_type:
                    # Exact PostprocessNode properties from original
                    properties = {
                        'output_format': node.get_property('output_format') if hasattr(node, 'get_property') else 'JSON',
                        'confidence_threshold': node.get_property('confidence_threshold') if hasattr(node, 'get_property') else 0.5,
                        'nms_threshold': node.get_property('nms_threshold') if hasattr(node, 'get_property') else 0.4,
                        'max_detections': node.get_property('max_detections') if hasattr(node, 'get_property') else 100
                    }
                elif 'Output' in node_type:
                    # Exact OutputNode properties from original
                    properties = {
                        'output_type': node.get_property('output_type') if hasattr(node, 'get_property') else 'File',
                        'destination': node.get_property('destination') if hasattr(node, 'get_property') else '',
                        'format': node.get_property('format') if hasattr(node, 'get_property') else 'JSON',
                        'save_interval': node.get_property('save_interval') if hasattr(node, 'get_property') else 1.0
                    }
            
            if properties:
                for prop_name, prop_value in properties.items():
                    # Create widget based on property type and name
                    widget = self.create_property_widget_enhanced(node, prop_name, prop_value)
                    
                    # Add to form
                    label = prop_name.replace('_', ' ').title()
                    form_layout.addRow(f"{label}:", widget)
            else:
                # Show available properties for debugging
                info_text = f"Node type: {node.__class__.__name__}\n"
                if hasattr(node, 'properties'):
                    props = node.properties()
                    info_text += f"Available properties: {list(props.keys())}"
                else:
                    info_text += "No properties method found"
                
                info_label = QLabel(info_text)
                info_label.setStyleSheet("color: #f9e2af; font-size: 10px;")
                form_layout.addRow(info_label)
                
        except Exception as e:
            error_label = QLabel(f"Error loading properties: {e}")
            error_label.setStyleSheet("color: #f38ba8;")
            form_layout.addRow(error_label)
            import traceback
            traceback.print_exc()
        
        self.node_props_layout.addWidget(form_widget)
    
    def create_property_widget(self, node, prop_name: str, prop_value, options: Dict):
        """Create appropriate widget for a property."""
        # Simple implementation - can be enhanced
        if isinstance(prop_value, bool):
            widget = QCheckBox()
            widget.setChecked(prop_value)
        elif isinstance(prop_value, int):
            widget = QSpinBox()
            widget.setValue(prop_value)
            if 'min' in options:
                widget.setMinimum(options['min'])
            if 'max' in options:
                widget.setMaximum(options['max'])
        elif isinstance(prop_value, float):
            widget = QDoubleSpinBox()
            widget.setValue(prop_value)
            if 'min' in options:
                widget.setMinimum(options['min'])
            if 'max' in options:
                widget.setMaximum(options['max'])
        elif isinstance(options, list):
            widget = QComboBox()
            widget.addItems(options)
            if prop_value in options:
                widget.setCurrentText(str(prop_value))
        else:
            widget = QLineEdit()
            widget.setText(str(prop_value))
        
        return widget
    
    def create_property_widget_enhanced(self, node, prop_name: str, prop_value):
        """Create enhanced property widget with better type detection."""
        # Create widget based on property name and value
        widget = None
        
        # Get property options from the node if available
        prop_options = None
        if hasattr(node, '_property_options') and prop_name in node._property_options:
            prop_options = node._property_options[prop_name]
        
        # Check for file path properties first (from prop_options or name pattern)
        if (prop_options and isinstance(prop_options, dict) and prop_options.get('type') == 'file_path') or \
           prop_name in ['model_path', 'source_path', 'destination']:
            # File path property with filters from prop_options or defaults
            widget = QPushButton(str(prop_value) if prop_value else 'Select File...')
            widget.setStyleSheet("text-align: left; padding: 5px;")
            
            def browse_file():
                # Use filter from prop_options if available, otherwise use defaults
                if prop_options and 'filter' in prop_options:
                    file_filter = prop_options['filter']
                else:
                    # Fallback to original filters
                    filters = {
                        'model_path': 'Model files (*.onnx *.tflite *.pb)',
                        'source_path': 'Media files (*.mp4 *.avi *.mov *.mkv *.wav *.mp3)',
                        'destination': 'Output files (*.json *.xml *.csv *.txt)'
                    }
                    file_filter = filters.get(prop_name, 'All files (*)')
                
                file_path, _ = QFileDialog.getOpenFileName(self, f'Select {prop_name}', '', file_filter)
                if file_path:
                    widget.setText(file_path)
                    if hasattr(node, 'set_property'):
                        node.set_property(prop_name, file_path)
            
            widget.clicked.connect(browse_file)
        
        # Check for dropdown properties (list options from prop_options or predefined)
        elif (prop_options and isinstance(prop_options, list)) or \
             prop_name in ['source_type', 'dongle_series', 'output_format', 'format', 'output_type', 'resolution']:
            # Dropdown property
            widget = QComboBox()
            
            # Use options from prop_options if available, otherwise use defaults
            if prop_options and isinstance(prop_options, list):
                items = prop_options
            else:
                # Fallback to original options
                options = {
                    'source_type': ['Camera', 'Microphone', 'File', 'RTSP Stream', 'HTTP Stream'],
                    'dongle_series': ['520', '720', '1080', 'Custom'],
                    'output_format': ['JSON', 'XML', 'CSV', 'Binary'],
                    'format': ['JSON', 'XML', 'CSV', 'Binary'],
                    'output_type': ['File', 'API Endpoint', 'Database', 'Display', 'MQTT'],
                    'resolution': ['640x480', '1280x720', '1920x1080', '3840x2160', 'Custom']
                }
                items = options.get(prop_name, [str(prop_value)])
            
            widget.addItems(items)
            
            if str(prop_value) in items:
                widget.setCurrentText(str(prop_value))
            
            def on_change(text):
                if hasattr(node, 'set_property'):
                    node.set_property(prop_name, text)
            
            widget.currentTextChanged.connect(on_change)
        
        elif isinstance(prop_value, bool):
            # Boolean property
            widget = QCheckBox()
            widget.setChecked(prop_value)
            
            def on_change(state):
                if hasattr(node, 'set_property'):
                    node.set_property(prop_name, state == 2)
            
            widget.stateChanged.connect(on_change)
        
        elif isinstance(prop_value, int):
            # Integer property
            widget = QSpinBox()
            widget.setValue(prop_value)
            
            # Set range from prop_options if available, otherwise use defaults
            if prop_options and isinstance(prop_options, dict) and 'min' in prop_options and 'max' in prop_options:
                widget.setRange(prop_options['min'], prop_options['max'])
            else:
                # Fallback to original ranges for specific properties
                widget.setRange(0, 99999)  # Default range
                if prop_name in ['device_id']:
                    widget.setRange(0, 10)
                elif prop_name in ['fps']:
                    widget.setRange(1, 120)
                elif prop_name in ['resize_width', 'resize_height']:
                    widget.setRange(64, 4096)
                elif prop_name in ['num_dongles']:
                    widget.setRange(1, 16)
                elif prop_name in ['max_detections']:
                    widget.setRange(1, 1000)
            
            def on_change(value):
                if hasattr(node, 'set_property'):
                    node.set_property(prop_name, value)
            
            widget.valueChanged.connect(on_change)
        
        elif isinstance(prop_value, float):
            # Float property
            widget = QDoubleSpinBox()
            widget.setValue(prop_value)
            widget.setDecimals(2)
            
            # Set range and step from prop_options if available, otherwise use defaults
            if prop_options and isinstance(prop_options, dict):
                if 'min' in prop_options and 'max' in prop_options:
                    widget.setRange(prop_options['min'], prop_options['max'])
                else:
                    widget.setRange(0.0, 999.0)  # Default range
                
                if 'step' in prop_options:
                    widget.setSingleStep(prop_options['step'])
                else:
                    widget.setSingleStep(0.01)  # Default step
            else:
                # Fallback to original ranges for specific properties
                widget.setRange(0.0, 999.0)  # Default range
                if prop_name in ['confidence_threshold', 'nms_threshold']:
                    widget.setRange(0.0, 1.0)
                    widget.setSingleStep(0.1)
                elif prop_name in ['save_interval']:
                    widget.setRange(0.1, 60.0)
                    widget.setSingleStep(0.1)
            
            def on_change(value):
                if hasattr(node, 'set_property'):
                    node.set_property(prop_name, value)
            
            widget.valueChanged.connect(on_change)
        
        else:
            # String property (default)
            widget = QLineEdit()
            widget.setText(str(prop_value))
            
            # Set placeholders for specific properties
            placeholders = {
                'model_path': 'Path to model file (.nef, .onnx, etc.)',
                'destination': 'Output file path',
                'resolution': 'e.g., 1920x1080'
            }
            
            if prop_name in placeholders:
                widget.setPlaceholderText(placeholders[prop_name])
            
            def on_change(text):
                if hasattr(node, 'set_property'):
                    node.set_property(prop_name, text)
            
            widget.textChanged.connect(on_change)
        
        return widget
    
    def clear_node_properties_panel(self):
        """Clear the node properties panel."""
        if not self.node_props_layout:
            return
        
        # Remove all widgets
        for i in reversed(range(self.node_props_layout.count())):
            child = self.node_props_layout.itemAt(i).widget()
            if child:
                child.deleteLater()
        
        # Show instructions and hide container
        self.node_props_container.setVisible(False)
        self.props_instructions.setVisible(True)
    
    
    def detect_dongles(self):
        """Detect available dongles using actual device scanning."""
        if not self.dongles_list:
            return
        
        self.dongles_list.clear()
        
        try:
            # Import MultiDongle for device scanning
            from cluster4npu_ui.core.functions.Multidongle import MultiDongle
            
            # Scan for available devices
            devices = MultiDongle.scan_devices()
            
            if devices:
                # Add detected devices to the list
                for device in devices:
                    port_id = device['port_id']
                    series = device['series']
                    self.dongles_list.addItem(f"{series} Dongle - Port {port_id}")
                
                # Add summary item
                self.dongles_list.addItem(f"Total: {len(devices)} device(s) detected")
                
                # Store device info for later use
                self.detected_devices = devices
                
            else:
                self.dongles_list.addItem("No Kneron devices detected")
                self.detected_devices = []
                
        except Exception as e:
            # Fallback to simulation if scanning fails
            self.dongles_list.addItem("Device scanning failed - using simulation")
            self.dongles_list.addItem("Simulated KL520 Dongle - Port 28")
            self.dongles_list.addItem("Simulated KL720 Dongle - Port 32")
            self.detected_devices = []
            
            # Print error for debugging
            print(f"Dongle detection error: {str(e)}")
    
    def get_detected_devices(self):
        """
        Get the list of detected devices with their port IDs and series.
        
        Returns:
            List[Dict]: List of device information with port_id and series
        """
        return getattr(self, 'detected_devices', [])
    
    def refresh_dongle_detection(self):
        """
        Refresh the dongle detection and update the UI.
        This can be called when dongles are plugged/unplugged.
        """
        self.detect_dongles()
        
        # Update any other UI components that depend on dongle detection
        self.update_performance_estimation()
    
    def get_available_ports(self):
        """
        Get list of available port IDs from detected devices.
        
        Returns:
            List[int]: List of available port IDs
        """
        return [device['port_id'] for device in self.get_detected_devices()]
    
    def get_device_by_port(self, port_id):
        """
        Get device information by port ID.
        
        Args:
            port_id (int): Port ID to search for
            
        Returns:
            Dict or None: Device information if found, None otherwise
        """
        for device in self.get_detected_devices():
            if device['port_id'] == port_id:
                return device
        return None
    
    def update_performance_estimation(self):
        """Update performance metrics based on pipeline and detected devices."""
        if not all([self.fps_label, self.latency_label, self.memory_label]):
            return
        
        # Enhanced performance estimation with device information
        if self.graph:
            num_nodes = len(self.graph.all_nodes())
            num_devices = len(self.get_detected_devices())
            
            # Base performance calculation
            base_fps = max(1, 60 - (num_nodes * 5))
            base_latency = num_nodes * 10
            base_memory = num_nodes * 50
            
            # Adjust for device availability
            if num_devices > 0:
                # More devices can potentially improve performance
                device_multiplier = min(1.5, 1 + (num_devices - 1) * 0.1)
                estimated_fps = int(base_fps * device_multiplier)
                estimated_latency = max(5, int(base_latency / device_multiplier))
                estimated_memory = base_memory  # Memory usage doesn't change much
            else:
                # No devices detected - show warning performance
                estimated_fps = 1
                estimated_latency = 999
                estimated_memory = base_memory
            
            self.fps_label.setText(f"{estimated_fps} FPS")
            self.latency_label.setText(f"{estimated_latency} ms")
            self.memory_label.setText(f"{estimated_memory} MB")
            
            if self.suggestions_text:
                suggestions = []
                
                # Device-specific suggestions
                if num_devices == 0:
                    suggestions.append("No Kneron devices detected. Connect dongles to enable inference.")
                elif num_devices < num_nodes:
                    suggestions.append(f"Consider connecting more devices ({num_devices} available, {num_nodes} pipeline stages).")
                
                # Performance suggestions
                if num_nodes > 5:
                    suggestions.append("Consider reducing the number of pipeline stages for better performance.")
                if estimated_fps < 30 and num_devices > 0:
                    suggestions.append("Current configuration may not achieve real-time performance.")
                
                # Hardware-specific suggestions
                detected_devices = self.get_detected_devices()
                if detected_devices:
                    device_series = set(device['series'] for device in detected_devices)
                    if len(device_series) > 1:
                        suggestions.append(f"Mixed device types detected: {', '.join(device_series)}. Performance may vary.")
                
                if not suggestions:
                    suggestions.append("Pipeline configuration looks good for optimal performance.")
                
                self.suggestions_text.setPlainText("\n".join(suggestions))
    
    def delete_selected_nodes(self):
        """Delete selected nodes from the graph."""
        if not self.graph:
            return
        
        selected_nodes = self.graph.selected_nodes()
        if selected_nodes:
            for node in selected_nodes:
                self.graph.delete_node(node)
            self.mark_modified()
    
    def validate_pipeline(self):
        """Validate the current pipeline."""
        if not self.graph:
            QMessageBox.information(self, "Validation", "No pipeline to validate.")
            return
        
        print("🔍 Validating pipeline...")
        summary = get_pipeline_summary(self.graph)
        
        if summary['valid']:
            print(f"Pipeline validation passed - {summary['stage_count']} stages, {summary['total_nodes']} nodes")
            QMessageBox.information(self, "Pipeline Validation", 
                                  f"Pipeline is valid!\n\n"
                                  f"Stages: {summary['stage_count']}\n"
                                  f"Total nodes: {summary['total_nodes']}")
        else:
            print(f"Pipeline validation failed: {summary['error']}")
            QMessageBox.warning(self, "Pipeline Validation", 
                              f"Pipeline validation failed:\n\n{summary['error']}")
    
    # File operations
    
    def new_pipeline(self):
        """Create a new pipeline."""
        if self.is_modified:
            reply = QMessageBox.question(self, "Save Changes", 
                                       "Save changes to current pipeline?",
                                       QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
            if reply == QMessageBox.Yes:
                self.save_pipeline()
            elif reply == QMessageBox.Cancel:
                return
        
        # Clear the graph
        if self.graph:
            self.graph.clear_session()
        
        self.project_name = "Untitled Pipeline"
        self.current_file = None
        self.is_modified = False
        self.update_window_title()
    
    def open_pipeline(self):
        """Open a pipeline file."""
        file_path, _ = QFileDialog.getOpenFileName(
            self, "Open Pipeline", 
            self.settings.get_default_project_location(),
            "Pipeline files (*.mflow);;All files (*)"
        )
        
        if file_path:
            self.load_pipeline_file(file_path)
    
    def save_pipeline(self):
        """Save the current pipeline."""
        if self.current_file:
            self.save_to_file(self.current_file)
        else:
            self.save_pipeline_as()
    
    def save_pipeline_as(self):
        """Save pipeline with a new name."""
        file_path, _ = QFileDialog.getSaveFileName(
            self, "Save Pipeline",
            os.path.join(self.settings.get_default_project_location(), f"{self.project_name}.mflow"),
            "Pipeline files (*.mflow)"
        )
        
        if file_path:
            self.save_to_file(file_path)
    
    def save_to_file(self, file_path: str):
        """Save pipeline to specified file."""
        try:
            pipeline_data = {
                'project_name': self.project_name,
                'description': self.description,
                'nodes': [],
                'connections': [],
                'version': '1.0'
            }
            
            # Save node data if graph is available
            if self.graph:
                for node in self.graph.all_nodes():
                    node_data = {
                        'id': node.id,
                        'name': node.name(),
                        'type': node.__class__.__name__,
                        'pos': node.pos()
                    }
                    if hasattr(node, 'get_business_properties'):
                        node_data['properties'] = node.get_business_properties()
                    pipeline_data['nodes'].append(node_data)
                
                # Save connections
                for node in self.graph.all_nodes():
                    for output_port in node.output_ports():
                        for input_port in output_port.connected_ports():
                            connection_data = {
                                'input_node': input_port.node().id,
                                'input_port': input_port.name(),
                                'output_node': node.id,
                                'output_port': output_port.name()
                            }
                            pipeline_data['connections'].append(connection_data)
            
            with open(file_path, 'w') as f:
                json.dump(pipeline_data, f, indent=2)
            
            self.current_file = file_path
            self.settings.add_recent_file(file_path)
            self.mark_saved()
            QMessageBox.information(self, "Saved", f"Pipeline saved to {file_path}")
            
        except Exception as e:
            QMessageBox.critical(self, "Save Error", f"Failed to save pipeline: {e}")
    
    def load_pipeline_file(self, file_path: str):
        """Load pipeline from file."""
        try:
            with open(file_path, 'r') as f:
                pipeline_data = json.load(f)
            
            self.project_name = pipeline_data.get('project_name', 'Loaded Pipeline')
            self.description = pipeline_data.get('description', '')
            self.current_file = file_path
            
            # Clear existing pipeline
            if self.graph:
                self.graph.clear_session()
            
            # Load nodes and connections
            self._load_nodes_from_data(pipeline_data.get('nodes', []))
            self._load_connections_from_data(pipeline_data.get('connections', []))
            
            self.settings.add_recent_file(file_path)
            self.mark_saved()
            self.update_window_title()
            
        except Exception as e:
            QMessageBox.critical(self, "Load Error", f"Failed to load pipeline: {e}")
    
    def export_configuration(self):
        """Export pipeline configuration."""
        QMessageBox.information(self, "Export", "Export functionality will be implemented in a future version.")
    
    def _load_nodes_from_data(self, nodes_data):
        """Load nodes from saved data."""
        if not self.graph:
            return
            
        # Import node types
        from core.nodes.exact_nodes import EXACT_NODE_TYPES
        
        # Create a mapping from class names to node classes
        class_to_node_type = {}
        for node_name, node_class in EXACT_NODE_TYPES.items():
            class_to_node_type[node_class.__name__] = node_class
        
        # Create a mapping from old IDs to new nodes
        self._node_id_mapping = {}
        
        for node_data in nodes_data:
            try:
                node_type = node_data.get('type')
                old_node_id = node_data.get('id')
                
                if node_type and node_type in class_to_node_type:
                    node_class = class_to_node_type[node_type]
                    
                    # Try different identifier formats
                    identifiers_to_try = [
                        node_class.__identifier__,
                        f"{node_class.__identifier__}.{node_class.__name__}",
                        node_class.__name__
                    ]
                    
                    node = None
                    for identifier in identifiers_to_try:
                        try:
                            node = self.graph.create_node(identifier)
                            break
                        except Exception:
                            continue
                    
                    if node:
                        # Map old ID to new node
                        if old_node_id:
                            self._node_id_mapping[old_node_id] = node
                            print(f"Mapped old ID {old_node_id} to new node {node.id}")
                        
                        # Set node properties
                        if 'name' in node_data:
                            node.set_name(node_data['name'])
                        if 'pos' in node_data:
                            node.set_pos(*node_data['pos'])
                        
                        # Restore business properties
                        if 'properties' in node_data:
                            for prop_name, prop_value in node_data['properties'].items():
                                try:
                                    node.set_property(prop_name, prop_value)
                                except Exception as e:
                                    print(f"Warning: Could not set property {prop_name}: {e}")
                                    
            except Exception as e:
                print(f"Error loading node {node_data}: {e}")
    
    def _load_connections_from_data(self, connections_data):
        """Load connections from saved data."""
        if not self.graph:
            return
            
        print(f"Loading {len(connections_data)} connections...")
        
        # Check if we have the node ID mapping
        if not hasattr(self, '_node_id_mapping'):
            print("  Warning: No node ID mapping available")
            return
        
        # Create connections between nodes
        for i, connection_data in enumerate(connections_data):
            try:
                input_node_id = connection_data.get('input_node')
                input_port_name = connection_data.get('input_port')
                output_node_id = connection_data.get('output_node')
                output_port_name = connection_data.get('output_port')
                
                print(f"Connection {i+1}: {output_node_id}:{output_port_name} -> {input_node_id}:{input_port_name}")
                
                # Find the nodes using the ID mapping
                input_node = self._node_id_mapping.get(input_node_id)
                output_node = self._node_id_mapping.get(output_node_id)
                
                if not input_node:
                    print(f"  Warning: Input node {input_node_id} not found in mapping")
                    continue
                if not output_node:
                    print(f"  Warning: Output node {output_node_id} not found in mapping")
                    continue
                
                # Get the ports
                input_port = input_node.get_input(input_port_name)
                output_port = output_node.get_output(output_port_name)
                
                if not input_port:
                    print(f"  Warning: Input port '{input_port_name}' not found on node {input_node.name()}")
                    continue
                if not output_port:
                    print(f"  Warning: Output port '{output_port_name}' not found on node {output_node.name()}")
                    continue
                
                # Create the connection - output connects to input
                output_port.connect_to(input_port)
                print(f"  ✓ Connection created successfully")
                        
            except Exception as e:
                print(f"Error loading connection {connection_data}: {e}")
    
    # State management
    
    def mark_modified(self):
        """Mark the pipeline as modified."""
        self.is_modified = True
        self.update_window_title()
        self.pipeline_modified.emit()
        
        # Schedule pipeline analysis
        self.schedule_analysis()
        
        # Update performance estimation when pipeline changes
        self.update_performance_estimation()
    
    def mark_saved(self):
        """Mark the pipeline as saved."""
        self.is_modified = False
        self.update_window_title()
    
    def update_window_title(self):
        """Update the window title."""
        title = f"Cluster4NPU - {self.project_name}"
        if self.is_modified:
            title += " *"
        if self.current_file:
            title += f" - {os.path.basename(self.current_file)}"
        self.setWindowTitle(title)
    
    def closeEvent(self, event):
        """Handle window close event."""
        if self.is_modified:
            reply = QMessageBox.question(self, "Save Changes",
                                       "Save changes before closing?",
                                       QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
            if reply == QMessageBox.Yes:
                self.save_pipeline()
                event.accept()
            elif reply == QMessageBox.No:
                event.accept()
            else:
                event.ignore()
        else:
            event.accept()
    
    # Pipeline Deployment
    
    def deploy_pipeline(self):
        """Deploy the current pipeline to dongles."""
        try:
            # First validate the pipeline
            if not self.validate_pipeline_for_deployment():
                return
            
            # Convert current pipeline to .mflow format
            pipeline_data = self.export_pipeline_data()
            
            # Show deployment dialog
            self.show_deployment_dialog(pipeline_data)
            
        except Exception as e:
            QMessageBox.critical(self, "Deployment Error", 
                               f"Failed to prepare pipeline for deployment: {str(e)}")
    
    def validate_pipeline_for_deployment(self) -> bool:
        """Validate pipeline is ready for deployment."""
        if not self.graph:
            QMessageBox.warning(self, "Deployment Error", 
                               "No pipeline to deploy. Please create a pipeline first.")
            return False
        
        # Check if pipeline has required nodes
        all_nodes = self.graph.all_nodes()
        if not all_nodes:
            QMessageBox.warning(self, "Deployment Error", 
                               "Pipeline is empty. Please add nodes to your pipeline.")
            return False
        
        # Check for required node types
        has_input = any(self.is_input_node(node) for node in all_nodes)
        has_model = any(self.is_model_node(node) for node in all_nodes)
        has_output = any(self.is_output_node(node) for node in all_nodes)
        
        if not has_input:
            QMessageBox.warning(self, "Deployment Error", 
                               "Pipeline must have at least one Input node.")
            return False
        
        if not has_model:
            QMessageBox.warning(self, "Deployment Error", 
                               "Pipeline must have at least one Model node.")
            return False
        
        if not has_output:
            QMessageBox.warning(self, "Deployment Error", 
                               "Pipeline must have at least one Output node.")
            return False
        
        # Validate model node configurations
        validation_errors = []
        for node in all_nodes:
            if self.is_model_node(node):
                errors = self.validate_model_node_for_deployment(node)
                validation_errors.extend(errors)
        
        if validation_errors:
            error_msg = "Please fix the following issues before deployment:\n\n"
            error_msg += "\n".join(f"• {error}" for error in validation_errors)
            QMessageBox.warning(self, "Deployment Validation", error_msg)
            return False
        
        return True
    
    def validate_model_node_for_deployment(self, node) -> List[str]:
        """Validate a model node for deployment requirements."""
        errors = []
        
        try:
            # Get node properties
            if hasattr(node, 'get_property'):
                model_path = node.get_property('model_path')
                scpu_fw_path = node.get_property('scpu_fw_path') 
                ncpu_fw_path = node.get_property('ncpu_fw_path')
                port_id = node.get_property('port_id')
            else:
                errors.append(f"Model node '{node.name()}' cannot read properties")
                return errors
            
            # Check model path
            if not model_path or not model_path.strip():
                errors.append(f"Model node '{node.name()}' missing model path")
            elif not os.path.exists(model_path):
                errors.append(f"Model file not found: {model_path}")
            elif not model_path.endswith('.nef'):
                errors.append(f"Model file must be .nef format: {model_path}")
            
            # Check firmware paths
            if not scpu_fw_path or not scpu_fw_path.strip():
                errors.append(f"Model node '{node.name()}' missing SCPU firmware path")
            elif not os.path.exists(scpu_fw_path):
                errors.append(f"SCPU firmware not found: {scpu_fw_path}")
            
            if not ncpu_fw_path or not ncpu_fw_path.strip():
                errors.append(f"Model node '{node.name()}' missing NCPU firmware path")
            elif not os.path.exists(ncpu_fw_path):
                errors.append(f"NCPU firmware not found: {ncpu_fw_path}")
            
            # Check port ID
            if not port_id or not port_id.strip():
                errors.append(f"Model node '{node.name()}' missing port ID")
            else:
                # Validate port ID format
                try:
                    port_ids = [int(p.strip()) for p in port_id.split(',') if p.strip()]
                    if not port_ids:
                        errors.append(f"Model node '{node.name()}' has invalid port ID format")
                except ValueError:
                    errors.append(f"Model node '{node.name()}' has invalid port ID: {port_id}")
            
        except Exception as e:
            errors.append(f"Error validating model node '{node.name()}': {str(e)}")
        
        return errors
    
    def export_pipeline_data(self) -> Dict[str, Any]:
        """Export current pipeline to dictionary format for deployment."""
        pipeline_data = {
            'project_name': self.project_name,
            'description': self.description,
            'nodes': [],
            'connections': [],
            'version': '1.0'
        }
        
        if not self.graph:
            return pipeline_data
        
        # Export nodes
        for node in self.graph.all_nodes():
            node_data = {
                'id': node.id,
                'name': node.name(),
                'type': node.__class__.__name__,
                'pos': node.pos(),
                'properties': {}
            }
            
            # Get node properties
            if hasattr(node, 'get_business_properties'):
                node_data['properties'] = node.get_business_properties()
            elif hasattr(node, '_property_options') and node._property_options:
                for prop_name in node._property_options.keys():
                    if hasattr(node, 'get_property'):
                        try:
                            node_data['properties'][prop_name] = node.get_property(prop_name)
                        except:
                            pass
            
            pipeline_data['nodes'].append(node_data)
        
        # Export connections
        for node in self.graph.all_nodes():
            if hasattr(node, 'output_ports'):
                for output_port in node.output_ports():
                    if hasattr(output_port, 'connected_ports'):
                        for input_port in output_port.connected_ports():
                            connection_data = {
                                'input_node': input_port.node().id,
                                'input_port': input_port.name(),
                                'output_node': node.id,
                                'output_port': output_port.name()
                            }
                            pipeline_data['connections'].append(connection_data)
        
        return pipeline_data
    
    def show_deployment_dialog(self, pipeline_data: Dict[str, Any]):
        """Show deployment dialog and handle deployment process."""
        from ..dialogs.deployment import DeploymentDialog
        
        dialog = DeploymentDialog(pipeline_data, parent=self)
        if dialog.exec_() == dialog.Accepted:
            # Deployment was successful or initiated
            self.statusBar().showMessage("Pipeline deployment initiated...", 3000)
    
    def is_input_node(self, node) -> bool:
        """Check if node is an input node."""
        return ('input' in str(type(node)).lower() or 
                hasattr(node, 'NODE_NAME') and 'input' in str(node.NODE_NAME).lower())
    
    def is_model_node(self, node) -> bool:
        """Check if node is a model node."""
        return ('model' in str(type(node)).lower() or 
                hasattr(node, 'NODE_NAME') and 'model' in str(node.NODE_NAME).lower())
    
    def is_output_node(self, node) -> bool:
        """Check if node is an output node."""
        return ('output' in str(type(node)).lower() or 
                hasattr(node, 'NODE_NAME') and 'output' in str(node.NODE_NAME).lower())