import sys
import json
import os

# Ensure QApplication exists before any widget creation
from PyQt5.QtWidgets import QApplication
if not QApplication.instance():
    app = QApplication(sys.argv)
else:
    app = QApplication.instance()

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

from NodeGraphQt import NodeGraph, BaseNode, PropertiesBinWidget

# Harmonious theme with complementary color palette
HARMONIOUS_THEME_STYLESHEET = """
    QWidget {
        background-color: #1e1e2e;
        color: #cdd6f4;
        font-family: "Inter", "SF Pro Display", "Segoe UI", sans-serif;
        font-size: 13px;
    }
    QMainWindow {
        background-color: #181825;
    }
    QDialog {
        background-color: #1e1e2e;
        border: 1px solid #313244;
    }
    QLabel {
        color: #f9e2af;
        font-weight: 500;
    }
    QLineEdit, QTextEdit, QSpinBox, QDoubleSpinBox, QComboBox {
        background-color: #313244;
        border: 2px solid #45475a;
        padding: 8px 12px;
        border-radius: 8px;
        color: #cdd6f4;
        selection-background-color: #74c7ec;
        font-size: 13px;
    }
    QLineEdit:focus, QTextEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus {
        border-color: #89b4fa;
        background-color: #383a59;
        outline: none;
        box-shadow: 0 0 0 3px rgba(137, 180, 250, 0.1);
    }
    QLineEdit:hover, QTextEdit:hover, QSpinBox:hover, QDoubleSpinBox:hover, QComboBox:hover {
        border-color: #585b70;
    }
    QPushButton {
        background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #89b4fa, stop:1 #74c7ec);
        color: #1e1e2e;
        border: none;
        padding: 10px 16px;
        border-radius: 8px;
        font-weight: 600;
        font-size: 13px;
        min-height: 16px;
    }
    QPushButton:hover {
        background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #a6c8ff, stop:1 #89dceb);
        transform: translateY(-1px);
    }
    QPushButton:pressed {
        background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #7287fd, stop:1 #5fb3d3);
    }
    QPushButton:disabled {
        background-color: #45475a;
        color: #6c7086;
    }
    QDialogButtonBox QPushButton {
        background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #89b4fa, stop:1 #74c7ec);
        color: #1e1e2e;
        min-width: 90px;
        margin: 2px;
    }
    QDialogButtonBox QPushButton:hover {
        background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #a6c8ff, stop:1 #89dceb);
    }
    QDialogButtonBox QPushButton[text="Cancel"] {
        background-color: #585b70;
        color: #cdd6f4;
        border: 1px solid #6c7086;
    }
    QDialogButtonBox QPushButton[text="Cancel"]:hover {
        background-color: #6c7086;
    }
    QListWidget {
        background-color: #313244;
        border: 2px solid #45475a;
        border-radius: 8px;
        outline: none;
    }
    QListWidget::item {
        padding: 12px;
        border-bottom: 1px solid #45475a;
        color: #cdd6f4;
        border-radius: 4px;
        margin: 2px;
    }
    QListWidget::item:selected {
        background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #89b4fa, stop:1 #74c7ec);
        color: #1e1e2e;
        border-radius: 6px;
    }
    QListWidget::item:hover {
        background-color: #383a59;
        border-radius: 6px;
    }
    QSplitter::handle {
        background-color: #45475a;
        width: 3px;
        height: 3px;
    }
    QSplitter::handle:hover {
        background-color: #89b4fa;
    }
    QCheckBox {
        color: #cdd6f4;
        spacing: 8px;
    }
    QCheckBox::indicator {
        width: 18px;
        height: 18px;
        border: 2px solid #45475a;
        border-radius: 4px;
        background-color: #313244;
    }
    QCheckBox::indicator:checked {
        background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #89b4fa, stop:1 #74c7ec);
        border-color: #89b4fa;
    }
    QCheckBox::indicator:hover {
        border-color: #89b4fa;
    }
    QScrollArea {
        border: none;
        background-color: #1e1e2e;
    }
    QScrollBar:vertical {
        background-color: #313244;
        width: 14px;
        border-radius: 7px;
        margin: 0px;
    }
    QScrollBar::handle:vertical {
        background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #89b4fa, stop:1 #74c7ec);
        border-radius: 7px;
        min-height: 20px;
        margin: 2px;
    }
    QScrollBar::handle:vertical:hover {
        background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #a6c8ff, stop:1 #89dceb);
    }
    QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
        border: none;
        background: none;
        height: 0px;
    }
    QMenuBar {
        background-color: #181825;
        color: #cdd6f4;
        border-bottom: 1px solid #313244;
        padding: 4px;
    }
    QMenuBar::item {
        padding: 8px 12px;
        background-color: transparent;
        border-radius: 6px;
    }
    QMenuBar::item:selected {
        background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #89b4fa, stop:1 #74c7ec);
        color: #1e1e2e;
    }
    QMenu {
        background-color: #313244;
        color: #cdd6f4;
        border: 1px solid #45475a;
        border-radius: 8px;
        padding: 4px;
    }
    QMenu::item {
        padding: 8px 16px;
        border-radius: 4px;
    }
    QMenu::item:selected {
        background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #89b4fa, stop:1 #74c7ec);
        color: #1e1e2e;
    }
    QComboBox::drop-down {
        border: none;
        width: 30px;
        border-radius: 4px;
    }
    QComboBox::down-arrow {
        image: none;
        border: 5px solid transparent;
        border-top: 6px solid #cdd6f4;
        margin-right: 8px;
    }
    QFormLayout QLabel {
        font-weight: 600;
        margin-bottom: 4px;
        color: #f9e2af;
    }
    QTextEdit {
        line-height: 1.4;
    }
    /* Custom accent colors for different UI states */
    .success {
        color: #a6e3a1;
    }
    .warning {
        color: #f9e2af;
    }
    .error {
        color: #f38ba8;
    }
    .info {
        color: #89b4fa;
    }
"""


# 1. 修改节点类，只使用简单的create_property
# 更新节点类，添加业务相关属性
class ModelNode(BaseNode):
    """Model node for ML inference"""
    
    __identifier__ = 'com.cluster.model_node'
    NODE_NAME = 'Model Node'
    
    def __init__(self):
        super(ModelNode, self).__init__()
        
        self.add_input('input', multi_input=False, color=(255, 140, 0))
        self.add_output('output', color=(0, 255, 0))
        self.set_color(65, 84, 102)
        
        # 业务属性
        self.create_property('model_path', '')
        self.create_property('dongle_series', '520')
        self.create_property('num_dongles', 1)
        self.create_property('port_id', '')
        
        # 属性选项和验证规则
        self._property_options = {
            'dongle_series': ['520', '720', '1080', 'Custom'],
            'num_dongles': {'min': 1, 'max': 16},
            'model_path': {'type': 'file_path', 'filter': 'Model files (*.onnx *.tflite *.pb)'},
            'port_id': {'placeholder': 'e.g., 8080 or auto'}
        }

class PreprocessNode(BaseNode):
    """Preprocessing node"""
    
    __identifier__ = 'com.cluster.preprocess_node'
    NODE_NAME = 'Preprocess Node'
    
    def __init__(self):
        super(PreprocessNode, self).__init__()
        
        self.add_input('input', multi_input=False, color=(255, 140, 0))
        self.add_output('output', color=(0, 255, 0))
        self.set_color(45, 126, 72)
        
        # 预处理业务属性
        self.create_property('resize_width', 640)
        self.create_property('resize_height', 480)
        self.create_property('normalize', True)
        self.create_property('crop_enabled', False)
        self.create_property('operations', 'resize,normalize')
        
        self._property_options = {
            'resize_width': {'min': 64, 'max': 4096},
            'resize_height': {'min': 64, 'max': 4096},
            'operations': {'placeholder': 'comma-separated: resize,normalize,crop'}
        }

class PostprocessNode(BaseNode):
    """Postprocessing node"""
    
    __identifier__ = 'com.cluster.postprocess_node'
    NODE_NAME = 'Postprocess Node'
    
    def __init__(self):
        super(PostprocessNode, self).__init__()
        
        self.add_input('input', multi_input=False, color=(255, 140, 0))
        self.add_output('output', color=(0, 255, 0))
        self.set_color(153, 51, 51)
        
        # 后处理业务属性
        self.create_property('output_format', 'JSON')
        self.create_property('confidence_threshold', 0.5)
        self.create_property('nms_threshold', 0.4)
        self.create_property('max_detections', 100)
        
        self._property_options = {
            'output_format': ['JSON', 'XML', 'CSV', 'Binary'],
            'confidence_threshold': {'min': 0.0, 'max': 1.0, 'step': 0.1},
            'nms_threshold': {'min': 0.0, 'max': 1.0, 'step': 0.1},
            'max_detections': {'min': 1, 'max': 1000}
        }

class InputNode(BaseNode):
    """Input data source node"""
    
    __identifier__ = 'com.cluster.input_node'
    NODE_NAME = 'Input Node'
    
    def __init__(self):
        super(InputNode, self).__init__()
        
        self.add_output('output', color=(0, 255, 0))
        self.set_color(83, 133, 204)
        
        # 输入源业务属性
        self.create_property('source_type', 'Camera')
        self.create_property('device_id', 0)
        self.create_property('source_path', '')
        self.create_property('resolution', '1920x1080')
        self.create_property('fps', 30)
        
        self._property_options = {
            'source_type': ['Camera', 'Microphone', 'File', 'RTSP Stream', 'HTTP Stream'],
            'device_id': {'min': 0, 'max': 10},
            'resolution': ['640x480', '1280x720', '1920x1080', '3840x2160', 'Custom'],
            'fps': {'min': 1, 'max': 120},
            'source_path': {'type': 'file_path', 'filter': 'Media files (*.mp4 *.avi *.mov *.mkv *.wav *.mp3)'}
        }

class OutputNode(BaseNode):
    """Output data sink node"""
    
    __identifier__ = 'com.cluster.output_node'
    NODE_NAME = 'Output Node'
    
    def __init__(self):
        super(OutputNode, self).__init__()
        
        self.add_input('input', multi_input=False, color=(255, 140, 0))
        self.set_color(255, 140, 0)
        
        # 输出业务属性
        self.create_property('output_type', 'File')
        self.create_property('destination', '')
        self.create_property('format', 'JSON')
        self.create_property('save_interval', 1.0)
        
        self._property_options = {
            'output_type': ['File', 'API Endpoint', 'Database', 'Display', 'MQTT'],
            'format': ['JSON', 'XML', 'CSV', 'Binary'],
            'destination': {'type': 'file_path', 'filter': 'Output files (*.json *.xml *.csv *.txt)'},
            'save_interval': {'min': 0.1, 'max': 60.0, 'step': 0.1}
        }

# 修复后的CustomPropertiesWidget
class CustomPropertiesWidget(QWidget):
    def __init__(self, graph):
        super().__init__()
        self.graph = graph
        self.current_node = None
        self.property_widgets = {}
        self.original_values = {}  # Store original values for reset functionality
        
        self.setup_ui()
        print("Connecting node selection changed signal...")
        self.graph.node_selection_changed.connect(self.on_selection_changed)
        print("Signal connected successfully")
    
    def setup_ui(self):
        layout = QVBoxLayout(self)
        
        # 标题
        self.title = QLabel("Business Properties")
        self.title.setFont(QFont("Arial", 12, QFont.Bold))
        self.title.setStyleSheet("color: #343a40; padding: 10px; background-color: #e9ecef; border-radius: 4px;")
        layout.addWidget(self.title)
        
        # 调试信息标签
        self.debug_label = QLabel("No node selected")
        self.debug_label.setStyleSheet("color: #6c757d; font-size: 10px; padding: 5px;")
        layout.addWidget(self.debug_label)
        
        # 滚动区域
        self.scroll_area = QScrollArea()
        self.properties_container = QWidget()
        self.properties_layout = QFormLayout(self.properties_container)
        self.properties_layout.setSpacing(8)
        
        self.scroll_area.setWidget(self.properties_container)
        self.scroll_area.setWidgetResizable(True)
        layout.addWidget(self.scroll_area)
        
        # 底部按钮
        self.button_layout = QHBoxLayout()
        self.apply_btn = QPushButton("Apply Changes")
        self.apply_btn.clicked.connect(self.apply_changes)
        self.reset_btn = QPushButton("Reset")
        self.reset_btn.clicked.connect(self.reset_properties)
        
        self.button_layout.addWidget(self.apply_btn)
        self.button_layout.addWidget(self.reset_btn)
        layout.addLayout(self.button_layout)
    
    def on_selection_changed(self):
        print("Selection changed event triggered")
        selected = self.graph.selected_nodes()
        print(f"Selected nodes: {[node.name() for node in selected]}")
        
        if selected:
            self.load_node_properties(selected[0])
        else:
            self.clear_properties()
    
    def clear_properties(self):
        print("Clearing properties...")
        for widget in self.property_widgets.values():
            widget.deleteLater()
        self.property_widgets.clear()
        
        for i in reversed(range(self.properties_layout.count())):
            item = self.properties_layout.itemAt(i)
            if item and item.widget():
                item.widget().deleteLater()
        
        self.title.setText("Business Properties")
        self.debug_label.setText("No node selected")
    
    def load_node_properties(self, node):
        print(f"Loading properties for node: {node.name()}")
        
        self.clear_properties()
        self.current_node = node
        
        self.title.setText(f"Business Properties - {node.name()}")
        
        # 从custom字典中获取业务属性
        try:
            all_properties = node.properties()
            print(f"All properties: {all_properties}")
            
            # 检查是否有custom字典
            if 'custom' in all_properties:
                custom_properties = all_properties['custom']
                print(f"Custom properties found: {custom_properties}")
                
                # Store original values for reset functionality
                self.original_values = custom_properties.copy()
                
                self.debug_label.setText(f"Found {len(custom_properties)} business properties")
                
                if not custom_properties:
                    no_props_label = QLabel("No business properties found for this node type.")
                    no_props_label.setStyleSheet("color: #dc3545; font-style: italic; padding: 10px;")
                    self.properties_layout.addRow(no_props_label)
                    return
                
                for prop_name, value in custom_properties.items():
                    try:
                        print(f"Property {prop_name} = {value}")
                        
                        widget = self.create_property_widget(prop_name, value, node)
                        if widget:
                            self.property_widgets[prop_name] = widget
                            label = QLabel(prop_name.replace('_', ' ').title() + ":")
                            label.setStyleSheet("font-weight: bold; color: #495057;")
                            self.properties_layout.addRow(label, widget)
                            print(f"Added widget for {prop_name}")
                    except Exception as e:
                        print(f"Error loading property {prop_name}: {e}")
                        import traceback
                        traceback.print_exc()
                        # 添加错误显示但不停止加载其他属性
                        error_label = QLabel(f"Error loading {prop_name}")
                        error_label.setStyleSheet("color: #dc3545; font-style: italic;")
                        self.properties_layout.addRow(f"{prop_name}:", error_label)
            else:
                print("No custom properties found")
                self.debug_label.setText("No custom properties found")
                self.original_values = {}
                no_props_label = QLabel("This node type has no configurable properties.")
                no_props_label.setStyleSheet("color: #6c757d; font-style: italic; padding: 10px;")
                self.properties_layout.addRow(no_props_label)
                
        except Exception as e:
            print(f"Error accessing node properties: {e}")
            import traceback
            traceback.print_exc()
            self.debug_label.setText("Error loading properties")
    
    def create_property_widget(self, prop_name, value, node):
        """根据属性类型和选项创建对应的输入控件"""
        print(f"Creating widget for {prop_name}, value: {value}")
        
        options = None
        if hasattr(node, '_property_options') and prop_name in node._property_options:
            options = node._property_options[prop_name]
            print(f"Found options for {prop_name}: {options}")
        
        # 文件路径选择器
        if isinstance(options, dict) and options.get('type') == 'file_path':
            container = QWidget()
            layout = QHBoxLayout(container)
            layout.setContentsMargins(0, 0, 0, 0)
            
            line_edit = QLineEdit()
            line_edit.setText(str(value))
            line_edit.setPlaceholderText(options.get('placeholder', 'Select file...'))
            
            browse_btn = QPushButton("Browse")
            browse_btn.setMaximumWidth(70)
            browse_btn.clicked.connect(lambda: self.browse_file(line_edit, options.get('filter', 'All files (*.*)')))
            
            layout.addWidget(line_edit)
            layout.addWidget(browse_btn)
            
            # 绑定变化事件 - 使用直接属性更新
            line_edit.textChanged.connect(lambda text: self.update_node_property(node, prop_name, text))
            return container
        
        # 下拉选择
        elif isinstance(options, list):
            widget = QComboBox()
            widget.addItems(options)
            if str(value) in options:
                widget.setCurrentText(str(value))
            widget.currentTextChanged.connect(lambda text: self.update_node_property(node, prop_name, text))
            return widget
        
        # 数值范围控件 - 修复类型转换
        elif isinstance(options, dict) and 'min' in options and 'max' in options:
            if isinstance(value, float) or options.get('step'):
                widget = QDoubleSpinBox()
                widget.setDecimals(1)
                widget.setSingleStep(options.get('step', 0.1))
                widget.setValue(float(value) if isinstance(value, (int, float)) else float(options['min']))
            else:
                widget = QSpinBox()
                # 修复: 确保传入int类型
                widget.setValue(int(value) if isinstance(value, (int, float)) else int(options['min']))
            
            widget.setRange(options['min'], options['max'])
            widget.valueChanged.connect(lambda val: self.update_node_property(node, prop_name, val))
            return widget
        
        # 布尔值
        elif isinstance(value, bool):
            widget = QCheckBox()
            widget.setChecked(value)
            widget.toggled.connect(lambda checked: self.update_node_property(node, prop_name, checked))
            return widget
        
        # 普通文本输入
        else:
            widget = QLineEdit()
            widget.setText(str(value))
            if isinstance(options, dict) and 'placeholder' in options:
                widget.setPlaceholderText(options['placeholder'])
            widget.textChanged.connect(lambda text: self.update_node_property(node, prop_name, text))
            return widget
    
    def update_node_property(self, node, prop_name, value):
        """更新节点属性 - 使用直接属性更新方式"""
        try:
            print(f"Updating {prop_name} = {value}")
            
            # 尝试直接设置属性（如果NodeGraphQt支持）
            try:
                node.set_property(prop_name, value)
                print(f"Successfully updated {prop_name} using set_property")
                return
            except Exception as e:
                print(f"set_property failed: {e}")
            
            # 如果直接设置失败，尝试通过节点内部更新
            # 直接修改节点的_property_changed属性（如果存在）
            if hasattr(node, '_model') and hasattr(node._model, '_custom_properties'):
                if not hasattr(node._model, '_custom_properties'):
                    node._model._custom_properties = {}
                node._model._custom_properties[prop_name] = value
                print(f"Updated via _custom_properties: {prop_name} = {value}")
                return
            
            # 最后的备用方案：直接修改properties字典（可能不持久化）
            properties = node.properties()
            if 'custom' in properties:
                properties['custom'][prop_name] = value
                print(f"Updated via properties dict: {prop_name} = {value}")
            
        except Exception as e:
            print(f"Error updating property {prop_name}: {e}")
            import traceback
            traceback.print_exc()
            
            # 显示错误但不崩溃
            QMessageBox.warning(self, "Property Update Error", 
                              f"Failed to update {prop_name}: {str(e)}\n\n"
                              f"Value will be reset when node is reselected.")
    
    def browse_file(self, line_edit, file_filter):
        """打开文件浏览对话框"""
        filename, _ = QFileDialog.getOpenFileName(self, "Select File", "", file_filter)
        if filename:
            line_edit.setText(filename)
    
    def apply_changes(self):
        """应用所有更改"""
        if self.current_node:
            # 尝试保存当前所有属性值
            try:
                saved_count = 0
                for prop_name, widget in self.property_widgets.items():
                    # 根据widget类型获取当前值
                    if isinstance(widget, QLineEdit):
                        value = widget.text()
                    elif isinstance(widget, QComboBox):
                        value = widget.currentText()
                    elif isinstance(widget, (QSpinBox, QDoubleSpinBox)):
                        value = widget.value()
                    elif isinstance(widget, QCheckBox):
                        value = widget.isChecked()
                    elif isinstance(widget, QWidget):  # 文件路径容器
                        line_edit = widget.findChild(QLineEdit)
                        value = line_edit.text() if line_edit else ""
                    else:
                        continue
                    
                    self.update_node_property(self.current_node, prop_name, value)
                    saved_count += 1
                
                QMessageBox.information(self, "Applied", 
                                      f"Applied {saved_count} properties to {self.current_node.name()}")
            except Exception as e:
                QMessageBox.warning(self, "Apply Error", f"Error applying changes: {str(e)}")
    
    def reset_properties(self):
        """Reset properties to original values when node was first loaded"""
        if self.current_node:
            reply = QMessageBox.question(self, "Reset Properties", 
                                       "Reset all properties to their original values?",
                                       QMessageBox.Yes | QMessageBox.No)
            if reply == QMessageBox.Yes:
                try:
                    # Use original values if available, otherwise fall back to defaults
                    values_to_restore = self.original_values if self.original_values else self.get_default_values_for_node(self.current_node)
                    
                    if not values_to_restore:
                        QMessageBox.information(self, "No Reset Available", 
                                              "No original or default values available to reset.")
                        return
                    
                    # Reset each property to its original/default value
                    reset_count = 0
                    for prop_name, original_value in values_to_restore.items():
                        try:
                            self.update_node_property(self.current_node, prop_name, original_value)
                            reset_count += 1
                            print(f"Reset {prop_name} to {original_value}")
                        except Exception as e:
                            print(f"Error resetting property {prop_name}: {e}")
                    
                    # Update the UI widgets to show the reset values
                    self.update_ui_widgets_with_values(values_to_restore)
                    
                    source_type = "original" if self.original_values else "default"
                    QMessageBox.information(self, "Reset Complete", 
                                          f"Reset {reset_count} properties to {source_type} values.")
                    
                except Exception as e:
                    QMessageBox.warning(self, "Reset Error", f"Error resetting properties: {str(e)}")
    
    def update_ui_widgets_with_values(self, values_dict):
        """Update the UI widgets to display the specified values"""
        for prop_name, value in values_dict.items():
            if prop_name in self.property_widgets:
                widget = self.property_widgets[prop_name]
                try:
                    # Update widget based on type
                    if isinstance(widget, QLineEdit):
                        widget.setText(str(value))
                    elif isinstance(widget, QComboBox):
                        widget.setCurrentText(str(value))
                    elif isinstance(widget, (QSpinBox, QDoubleSpinBox)):
                        widget.setValue(value)
                    elif isinstance(widget, QCheckBox):
                        widget.setChecked(bool(value))
                    elif isinstance(widget, QWidget):  # File path container
                        line_edit = widget.findChild(QLineEdit)
                        if line_edit:
                            line_edit.setText(str(value))
                except Exception as e:
                    print(f"Error updating widget for {prop_name}: {e}")
    
    def get_default_values_for_node(self, node):
        """Get the default property values for a specific node type"""
        # Define default values for each node type
        defaults = {
            'ModelNode': {
                'model_path': '',
                'dongle_series': '520',
                'num_dongles': 1,
                'port_id': ''
            },
            'PreprocessNode': {
                'resize_width': 640,
                'resize_height': 480,
                'normalize': True,
                'crop_enabled': False,
                'operations': 'resize,normalize'
            },
            'PostprocessNode': {
                'output_format': 'JSON',
                'confidence_threshold': 0.5,
                'nms_threshold': 0.4,
                'max_detections': 100
            },
            'InputNode': {
                'source_type': 'Camera',
                'device_id': 0,
                'source_path': '',
                'resolution': '1920x1080',
                'fps': 30
            },
            'OutputNode': {
                'output_type': 'File',
                'destination': '',
                'format': 'JSON',
                'save_interval': 1.0
            }
        }
        
        # Get the node class name
        node_class_name = node.__class__.__name__
        
        # Return the defaults for this node type, or empty dict if not found
        return defaults.get(node_class_name, {})

class CreatePipelineDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Create New Pipeline")
        self.setMinimumWidth(450)
        self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET)

        self.projectNameInput = QLineEdit(self)
        self.descriptionInput = QTextEdit(self)
        self.descriptionInput.setPlaceholderText("Optional (briefly describe the pipeline's purpose)")
        self.descriptionInput.setFixedHeight(80)

        formLayout = QFormLayout()
        formLayout.setSpacing(10)
        formLayout.addRow("Project Name:", self.projectNameInput)
        formLayout.addRow("Description:", self.descriptionInput)

        self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        self.buttonBox.accepted.connect(self.handle_ok)
        self.buttonBox.rejected.connect(self.reject)

        mainLayout = QVBoxLayout(self)
        mainLayout.setContentsMargins(15, 15, 15, 15)
        mainLayout.addLayout(formLayout)
        mainLayout.addSpacing(10)
        mainLayout.addWidget(self.buttonBox)

    def handle_ok(self):
        if not self.projectNameInput.text().strip():
            QMessageBox.warning(self, "Input Error", "Project Name cannot be empty.")
            return
        self.accept()

    def get_data(self):
        return {
            "project_name": self.projectNameInput.text().strip(),
            "description": self.descriptionInput.toPlainText().strip()
        }

class SimplePropertiesDialog(QDialog): # Corrected and placed before PipelineEditor
    """Simple properties dialog as fallback"""
    
    def __init__(self, node, parent=None):
        super().__init__(parent)
        self.node = node
        self.setWindowTitle(f"Properties - {node.name()}")
        self.setMinimumWidth(400)
        self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET)
        
        layout = QVBoxLayout(self)
        
        # Node info
        info_label = QLabel(f"Node: {node.name()}\nType: {node.type_}")
        info_label.setFont(QFont("Arial", 12, QFont.Bold))
        layout.addWidget(info_label)
        
        # Properties
        form_layout = QFormLayout()
        self.property_widgets = {}
        
        try:
            if hasattr(node, 'properties'):
                for prop_name in node.properties():
                    try:
                        current_value = node.get_property(prop_name)
                        
                        # Create simple text input for all properties
                        widget = QLineEdit()
                        widget.setText(str(current_value))
                        
                        self.property_widgets[prop_name] = widget
                        form_layout.addRow(f"{prop_name}:", widget)
                        
                    except Exception as e:
                        print(f"Error loading property {prop_name} for node {node.name()}: {e}")
                        # Optionally add a label to the dialog indicating error for this property
                        error_prop_label = QLabel(f"(Error loading {prop_name})")
                        form_layout.addRow(f"{prop_name}:", error_prop_label)

        except Exception as e:
            print(f"Error loading properties for node {node.name()}: {e}")
            error_label = QLabel(f"Error loading properties: {e}")
            layout.addWidget(error_label)
        
        layout.addLayout(form_layout)
        
        # Buttons
        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        button_box.accepted.connect(self.save_properties)
        button_box.rejected.connect(self.reject)
        layout.addWidget(button_box)
    
    def save_properties(self):
        """Save property changes"""
        try:
            for prop_name, widget in self.property_widgets.items():
                if not isinstance(widget, QLineEdit): # Skip if widget is not an input (e.g. error label)
                    continue
                text_value = widget.text()
                
                # Try to convert to appropriate type
                try:
                    # Get original value to determine type
                    original_value = self.node.get_property(prop_name) # Assuming this doesn't error again
                    if isinstance(original_value, bool):
                        value = text_value.lower() in ('true', '1', 'yes', 'on')
                    elif isinstance(original_value, int):
                        value = int(text_value)
                    elif isinstance(original_value, float):
                        value = float(text_value)
                    else:
                        value = text_value
                except (ValueError, TypeError): # Fallback if conversion fails
                    value = text_value
                except Exception as e_get_prop: # Handle case where get_property failed during save_properties' type check
                    print(f"Could not get original property type for {prop_name} during save. Saving as string. Error: {e_get_prop}")
                    value = text_value # Save as string if original type unknown
                
                # Set the property
                self.node.set_property(prop_name, value)
            
            self.accept()
            
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to save properties: {str(e)}")

class NodePalette(QWidget):
    """Node palette for adding nodes"""
    def __init__(self, graph):
        super().__init__()
        self.graph = graph
        self.setup_ui()
    
    def setup_ui(self):
        layout = QVBoxLayout(self)
        
        title = QLabel("Node Palette")
        title.setFont(QFont("Arial", 12, QFont.Bold))
        layout.addWidget(title)
        
        # Node creation buttons
        nodes = [
            ("Input", InputNode),
            ("Model", ModelNode),
            ("Preprocess", PreprocessNode),
            ("Postprocess", PostprocessNode),
            ("Output", OutputNode)
        ]
        
        for name, node_class in nodes:
            btn = QPushButton(name)
            btn.clicked.connect(lambda checked, cls=node_class: self.create_node(cls))
            layout.addWidget(btn)
        
        # Instructions
        instructions = QLabel()
        instructions.setText(
            "📍 Click buttons to add nodes\n"
            "⌨️ Or press TAB in graph area to search nodes\n"
            "🔗 Drag from 🟢GREEN output to 🟠ORANGE input\n" 
            "⚙️ Double-click nodes to see properties\n"
            "🗑️ Select + Delete key to remove\n"
            "💾 Ctrl+S to save\n"
            "🎯 Right-click for context menu\n"
            "📋 View menu → Properties Panel\n"
            "🔍 Debug menu → Show Registered Nodes"
        )
        instructions.setWordWrap(True)
        instructions.setStyleSheet("color: #6c757d; font-size: 10px; margin: 10px 0; padding: 10px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;")
        layout.addWidget(instructions)
        
        layout.addStretch()
    
    def create_node(self, node_class):
        """Create a new node and add to graph"""
        try:
            # Use class name instead of NODE_NAME for identifier
            node_id = f"{node_class.__identifier__}.{node_class.__name__}"
            # print(f"Creating node: {node_id}") # Optional: for debugging
            node = self.graph.create_node(node_id)
            if node: # Check if node was created
                 node.set_pos(100, 100)  # Position new nodes at visible location
                 # print(f"Successfully created node: {node_id}") # Optional: for debugging
            else:
                # This case might happen if registration was incomplete or ID is wrong.
                raise Exception(f"Graph returned None for create_node with ID: {node_id}")

        except Exception as e:
            print(f"Failed to create node {node_class.__name__}: {e}")
            QMessageBox.warning(self, "Node Creation Error", 
                              f"Failed to create {node_class.__name__}: {str(e)}\n\n"
                              f"Try using Tab key to search for nodes, or check console for registration errors.")

class IntegratedPipelineDashboard(QMainWindow):
    """Integrated dashboard combining pipeline editor, stage configuration, and performance estimation"""
    
    def __init__(self, project_name="", description="", filename=None):
        super().__init__()
        self.project_name = project_name
        self.description = description
        self.current_file = filename
        self.is_modified = False
        
        # Initialize attributes that will be used by methods
        self.allocation_layout = None
        self.stage_configs_layout = None
        self.stages_spinbox = None
        self.dongles_list = None
        self.fps_label = None
        self.latency_label = None
        self.memory_label = None
        self.suggestions_text = None
        self.props_instructions = None
        self.node_props_container = None
        self.node_props_layout = None
        
        # Initialize node graph
        self.graph = NodeGraph(properties_bin_class=None)
        
        # Register custom nodes
        nodes_to_register = [InputNode, ModelNode, PreprocessNode, PostprocessNode, OutputNode]
        for node_class in nodes_to_register:
            try:
                self.graph.register_node(node_class)
                # Uncomment for debugging: print(f"✅ Registered {node_class.__name__}")
            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.property_changed.connect(self.mark_modified)
        
        if hasattr(self.graph, 'port_connected'):
            self.graph.port_connected.connect(self.on_port_connected)
        if hasattr(self.graph, 'port_disconnected'):
            self.graph.port_disconnected.connect(self.on_port_disconnected)
        
        self.setup_integrated_ui()
        self.setup_menu()
        
        # Add keyboard shortcut for delete
        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)
        
        self.update_window_title()
        self.setGeometry(50, 50, 2000, 1200)  # Wider window for 3-panel layout
        self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET)

    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 horizontal splitter with 3 panels
        main_splitter = QSplitter(Qt.Horizontal)
        main_splitter.setStyleSheet("""
            QSplitter::handle {
                background-color: #45475a;
                width: 3px;
            }
            QSplitter::handle:hover {
                background-color: #89b4fa;
            }
        """)
        
        # Left side: Node Template Panel (20% width)
        left_panel = self.create_node_template_panel()
        left_panel.setMinimumWidth(250)
        left_panel.setMaximumWidth(350)
        
        # Middle: Pipeline Editor (50% width)
        editor_widget = QWidget()
        editor_layout = QVBoxLayout(editor_widget)
        editor_layout.setContentsMargins(5, 5, 5, 5)
        
        # Add pipeline editor title
        editor_title = QLabel("Pipeline Editor")
        editor_title.setStyleSheet("color: #f9e2af; font-size: 16px; font-weight: bold; padding: 10px;")
        editor_layout.addWidget(editor_title)
        
        # Add the node graph widget
        graph_widget = self.graph.widget
        graph_widget.setMinimumHeight(400)
        editor_layout.addWidget(graph_widget)
        
        # Right side: Configuration panels (30% width)
        right_panel = QWidget()
        right_panel.setMinimumWidth(350)
        right_panel.setMaximumWidth(450)
        right_layout = QVBoxLayout(right_panel)
        right_layout.setContentsMargins(5, 5, 5, 5)
        right_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;
            }
        """)
        
        # Node Properties Tab (most important for editing)
        node_props_panel = self.create_node_properties_panel()
        config_tabs.addTab(node_props_panel, "📝 Properties")
        
        # Stage Configuration Tab
        stage_config = self.create_stage_config_panel()
        config_tabs.addTab(stage_config, "⚙️ Stages")
        
        # Performance Estimation Tab
        performance_panel = self.create_performance_panel()
        config_tabs.addTab(performance_panel, "📊 Performance")
        
        # Dongle Management Tab
        dongle_panel = self.create_dongle_panel()
        config_tabs.addTab(dongle_panel, "🔌 Dongles")
        
        right_layout.addWidget(config_tabs)
        
        # Add widgets to splitter
        main_splitter.addWidget(left_panel)
        main_splitter.addWidget(editor_widget)
        main_splitter.addWidget(right_panel)
        main_splitter.setSizes([300, 800, 400])  # 20-50-30 split
        
        # Set main layout
        main_layout = QVBoxLayout(central_widget)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.addWidget(main_splitter)

    def create_node_template_panel(self):
        """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
        nodes_info = [
            ("🎯 Input Node", "Data input source", InputNode),
            ("🧠 Model Node", "AI inference model", ModelNode), 
            ("⚙️ Preprocess Node", "Data preprocessing", PreprocessNode),
            ("🔧 Postprocess Node", "Output processing", PostprocessNode),
            ("📤 Output Node", "Final output", OutputNode)
        ]
        
        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)
        
        # 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_node_properties_panel(self):
        """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 (will be populated when node is selected)
        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)
        
        # Connect to node selection changes
        self.graph.node_selection_changed.connect(self.update_node_properties_panel)
        
        return widget

    def add_node_to_graph(self, node_class):
        """Add a new node to the graph"""
        try:
            # Create node instance directly and add to graph
            node = node_class()
            self.graph.add_node(node)
            
            # Position it in a reasonable location (center with some randomness)
            import random
            x = random.randint(-100, 100)
            y = random.randint(-100, 100)
            node.set_pos(x, y)
            
            print(f"✅ Added {node_class.__name__} to graph at position ({x}, {y})")
            self.mark_modified()
            
            # Auto-update performance estimation
            self.update_performance_estimation()
            
        except Exception as e:
            print(f"❌ Failed to add {node_class.__name__}: {e}")
            import traceback
            traceback.print_exc()
            QMessageBox.warning(self, "Error", f"Failed to add {node_class.__name__}:\n{str(e)}")

    def update_node_properties_panel(self):
        """Update the node properties panel based on selected node"""
        try:
            # Clear existing properties
            for i in reversed(range(self.node_props_layout.count())):
                child = self.node_props_layout.itemAt(i).widget()
                if child:
                    child.setParent(None)
            
            selected_nodes = self.graph.selected_nodes()
            
            if not selected_nodes:
                # Show instructions when no node selected
                self.props_instructions.setVisible(True)
                self.node_props_container.setVisible(False)
                return
            
            # Hide instructions and show properties
            self.props_instructions.setVisible(False)
            self.node_props_container.setVisible(True)
            
            # Get first selected node
            node = selected_nodes[0]
            
            # Create properties form for the node
            self.create_node_properties_form(node)
            
        except Exception as e:
            print(f"Error updating node properties panel: {e}")

    def create_node_properties_form(self, node):
        """Create properties form for a specific node"""
        try:
            # Get node name safely
            try:
                node_name = node.name() if callable(node.name) else str(node.name)
            except:
                node_name = "Unknown Node"
            
            # Get node type safely with clean display names
            try:
                raw_type = node.type_() if callable(node.type_) else str(getattr(node, 'type_', 'Unknown'))
                
                # Check if it has a clean NODE_NAME attribute first
                if hasattr(node, 'NODE_NAME'):
                    node_type = node.NODE_NAME
                else:
                    # Extract clean name from full identifier or class name
                    if 'com.cluster.' in raw_type:
                        # Extract from full identifier like com.cluster.input_node.ExactInputNode
                        if raw_type.endswith('.ExactInputNode'):
                            node_type = 'Input Node'
                        elif raw_type.endswith('.ExactModelNode'):
                            node_type = 'Model Node'
                        elif raw_type.endswith('.ExactPreprocessNode'):
                            node_type = 'Preprocess Node'
                        elif raw_type.endswith('.ExactPostprocessNode'):
                            node_type = 'Postprocess Node'
                        elif raw_type.endswith('.ExactOutputNode'):
                            node_type = 'Output Node'
                        else:
                            # Fallback: extract base name
                            parts = raw_type.split('.')
                            if len(parts) >= 3:
                                base_name = parts[2].replace('_', ' ').title() + ' Node'
                                node_type = base_name
                            else:
                                node_type = raw_type
                    else:
                        # Extract from class name like ExactInputNode
                        class_name = node.__class__.__name__
                        if class_name.startswith('Exact') and class_name.endswith('Node'):
                            # Remove 'Exact' prefix and add space before 'Node'
                            clean_name = class_name[5:]  # Remove 'Exact'
                            if clean_name == 'InputNode':
                                node_type = 'Input Node'
                            elif clean_name == 'ModelNode':
                                node_type = 'Model Node'
                            elif clean_name == 'PreprocessNode':
                                node_type = 'Preprocess Node'
                            elif clean_name == 'PostprocessNode':
                                node_type = 'Postprocess Node'
                            elif clean_name == 'OutputNode':
                                node_type = 'Output Node'
                            else:
                                node_type = clean_name
                        else:
                            node_type = class_name
            except:
                node_type = "Unknown Type"
            
            # Node info header
            info_group = QGroupBox(f"{node_name} Properties")
            info_layout = QFormLayout(info_group)
            
            # Node name (editable)
            name_edit = QLineEdit(node_name)
            def update_node_name(text):
                try:
                    if hasattr(node, 'set_name') and callable(node.set_name):
                        node.set_name(text)
                    self.mark_modified()
                except Exception as e:
                    print(f"Error updating node name: {e}")
            
            name_edit.textChanged.connect(update_node_name)
            info_layout.addRow("Name:", name_edit)
            
            # Node type (read-only)
            type_label = QLabel(node_type)
            type_label.setStyleSheet("color: #a6adc8;")
            info_layout.addRow("Type:", type_label)
            
            self.node_props_layout.addWidget(info_group)
            
            # Get node properties - NodeGraphQt uses different property access methods
            custom_props = {}
            
            # Method 0: Try to get business properties first (highest priority)
            try:
                if hasattr(node, 'get_business_properties') and callable(node.get_business_properties):
                    business_props = node.get_business_properties()
                    if business_props:
                        custom_props = business_props
                        print(f"Found properties via get_business_properties(): {list(custom_props.keys())}")
                elif hasattr(node, '_business_properties') and node._business_properties:
                    custom_props = node._business_properties.copy()
                    print(f"Found properties via _business_properties: {list(custom_props.keys())}")
                
                # Check if node has a custom display properties method
                if hasattr(node, 'get_display_properties') and callable(node.get_display_properties):
                    display_props = node.get_display_properties()
                    if display_props and custom_props:
                        # Filter to only show the specified display properties
                        filtered_props = {k: v for k, v in custom_props.items() if k in display_props}
                        if filtered_props:
                            custom_props = filtered_props
                            print(f"Filtered to display properties: {list(custom_props.keys())}")
                        
            except Exception as e:
                print(f"Method 0 - get_business_properties() failed: {e}")
            
            # Method 1: Try to get properties from NodeGraphQt node (only if no business properties found)
            if not custom_props:
                try:
                    if hasattr(node, 'properties'):
                        # Get all properties from the node
                        all_props = node.properties()
                        print(f"All node properties: {list(all_props.keys())}")
                        
                        # Check if there's a 'custom' property that contains our properties
                        if 'custom' in all_props:
                            custom_value = all_props['custom']
                            print(f"Custom property value type: {type(custom_value)}, value: {custom_value}")
                            
                            # If custom property contains a dict, use it
                            if isinstance(custom_value, dict):
                                custom_props = custom_value
                            # If custom property is accessible via get_property, try that
                            elif hasattr(node, 'get_property'):
                                try:
                                    custom_from_get = node.get_property('custom')
                                    if isinstance(custom_from_get, dict):
                                        custom_props = custom_from_get
                                except:
                                    pass
                        
                        # Also include other potentially useful properties
                        useful_props = {}
                        for k, v in all_props.items():
                            if k not in {'name', 'id', 'selected', 'disabled', 'visible', 'pos', 'color', 
                                       'type_', 'icon', 'border_color', 'text_color', 'width', 'height', 
                                       'layout_direction', 'port_deletion_allowed', 'subgraph_session'}:
                                useful_props[k] = v
                        
                        # Merge custom_props with other useful properties
                        if useful_props:
                            custom_props.update(useful_props)
                        
                        print(f"Found properties via node.properties(): {list(custom_props.keys())}")
                except Exception as e:
                    print(f"Method 1 - node.properties() failed: {e}")
            
            # Method 2: Try to access properties via get_property (for NodeGraphQt created properties)
            # This should work for properties created with create_property()
            if not custom_props:
                try:
                    # Get all properties defined for this node type
                    if hasattr(node, 'get_property'):
                        # Define properties for different node types
                        node_type_properties = {
                            'ModelNode': ['model_path', 'dongle_series', 'num_dongles', 'port_id'],
                            'ExactModelNode': ['model_path', 'dongle_series', 'num_dongles', 'port_id'],
                            'InputNode': ['input_path', 'source_type', 'fps', 'source_path'],
                            'ExactInputNode': ['source_type', 'device_id', 'source_path', 'resolution', 'fps'],
                            'OutputNode': ['output_path', 'output_format', 'save_results'],
                            'ExactOutputNode': ['output_type', 'destination', 'format', 'save_interval'],
                            'PreprocessNode': ['resize_width', 'resize_height', 'operations'],
                            'ExactPreprocessNode': ['resize_width', 'resize_height', 'normalize', 'crop_enabled', 'operations'],
                            'PostprocessNode': ['confidence_threshold', 'nms_threshold', 'max_detections'],
                            'ExactPostprocessNode': ['output_format', 'confidence_threshold', 'nms_threshold', 'max_detections']
                        }
                        
                        # Try to determine node type
                        node_class_name = node.__class__.__name__
                        properties_to_check = []
                        
                        if node_class_name in node_type_properties:
                            properties_to_check = node_type_properties[node_class_name]
                        else:
                            # Try all known properties if we can't determine the type
                            properties_to_check = []
                            for props_list in node_type_properties.values():
                                properties_to_check.extend(props_list)
                        
                        print(f"Checking properties for {node_class_name}: {properties_to_check}")
                        
                        for prop in properties_to_check:
                            try:
                                value = node.get_property(prop)
                                # Only add if the property actually exists (not None and not raising exception)
                                custom_props[prop] = value
                                print(f"  Found {prop}: {value}")
                            except Exception as prop_error:
                                # Property doesn't exist for this node, which is expected
                                pass
                        
                        print(f"Found properties via get_property(): {list(custom_props.keys())}")
                except Exception as e:
                    print(f"Method 2 - get_property() failed: {e}")
            
            # Method 3: Try to access custom attribute if it exists
            if not custom_props:
                try:
                    if hasattr(node, 'custom'):
                        custom_attr = node.custom if not callable(node.custom) else node.custom()
                        if isinstance(custom_attr, dict):
                            custom_props = custom_attr
                            print(f"Found properties via custom attribute: {list(custom_props.keys())}")
                except Exception as e:
                    print(f"Method 3 - custom attribute failed: {e}")
            
            if custom_props:
                custom_group = QGroupBox("Custom Properties")
                custom_layout = QFormLayout(custom_group)
                
                for prop_name, prop_value in custom_props.items():
                    if prop_name in ['model_path', 'input_path', 'output_path']:
                        # File path property
                        path_layout = QHBoxLayout()
                        path_edit = QLineEdit(str(prop_value))
                        browse_btn = QPushButton("Browse...")
                        browse_btn.clicked.connect(lambda checked, pe=path_edit, pn=prop_name: self.browse_file_for_property(pe, pn))
                        
                        path_layout.addWidget(path_edit)
                        path_layout.addWidget(browse_btn)
                        
                        path_widget = QWidget()
                        path_widget.setLayout(path_layout)
                        custom_layout.addRow(prop_name.replace("_", " ").title() + ":", path_widget)
                        
                        # Connect to update node property
                        path_edit.textChanged.connect(lambda text, pn=prop_name: self.update_node_property(node, pn, text))
                        
                    elif isinstance(prop_value, (int, float)):
                        # Numeric property
                        if isinstance(prop_value, int):
                            spin_box = QSpinBox()
                            spin_box.setRange(-999999, 999999)
                            spin_box.setValue(prop_value)
                            spin_box.valueChanged.connect(lambda val, pn=prop_name: self.update_node_property(node, pn, val))
                        else:
                            spin_box = QDoubleSpinBox()
                            spin_box.setRange(-999999.0, 999999.0)
                            spin_box.setValue(prop_value)
                            spin_box.valueChanged.connect(lambda val, pn=prop_name: self.update_node_property(node, pn, val))
                        
                        custom_layout.addRow(prop_name.replace("_", " ").title() + ":", spin_box)
                        
                    elif isinstance(prop_value, bool):
                        # Boolean property
                        check_box = QCheckBox()
                        check_box.setChecked(prop_value)
                        check_box.toggled.connect(lambda checked, pn=prop_name: self.update_node_property(node, pn, checked))
                        custom_layout.addRow(prop_name.replace("_", " ").title() + ":", check_box)
                        
                    else:
                        # String property
                        text_edit = QLineEdit(str(prop_value))
                        text_edit.textChanged.connect(lambda text, pn=prop_name: self.update_node_property(node, pn, text))
                        custom_layout.addRow(prop_name.replace("_", " ").title() + ":", text_edit)
                
                self.node_props_layout.addWidget(custom_group)
            
            # Position info (read-only)
            pos_group = QGroupBox("Position")
            pos_layout = QFormLayout(pos_group)
            
            # Get position safely
            try:
                pos = node.pos() if callable(node.pos) else node.pos
                if isinstance(pos, (list, tuple)) and len(pos) >= 2:
                    x, y = pos[0], pos[1]
                else:
                    x, y = 0.0, 0.0
            except Exception as e:
                print(f"Error getting node position: {e}")
                x, y = 0.0, 0.0
            
            x_label = QLabel(f"{x:.1f}")
            y_label = QLabel(f"{y:.1f}")
            x_label.setStyleSheet("color: #a6adc8;")
            y_label.setStyleSheet("color: #a6adc8;")
            
            pos_layout.addRow("X:", x_label)
            pos_layout.addRow("Y:", y_label)
            
            self.node_props_layout.addWidget(pos_group)
            
        except Exception as e:
            print(f"Error creating properties form: {e}")
            error_label = QLabel(f"Error loading properties: {str(e)}")
            error_label.setStyleSheet("color: #f38ba8;")
            self.node_props_layout.addWidget(error_label)

    def update_node_property(self, node, property_name, value):
        """Update a node's custom property"""
        try:
            success = False
            
            # Method 1: Try NodeGraphQt set_property
            if hasattr(node, 'set_property') and callable(node.set_property):
                try:
                    node.set_property(property_name, value)
                    success = True
                    print(f"✅ Updated via set_property: {property_name} = {value}")
                except Exception as e:
                    print(f"set_property failed: {e}")
            
            # Method 2: Try custom attribute
            if not success and hasattr(node, 'custom'):
                try:
                    if not callable(node.custom):
                        node.custom[property_name] = value
                        success = True
                        print(f"✅ Updated via custom attribute: {property_name} = {value}")
                    else:
                        print(f"Warning: Cannot update property {property_name} - custom is a method")
                except Exception as e:
                    print(f"custom attribute update failed: {e}")
            
            # Method 3: Try direct attribute setting
            if not success:
                try:
                    setattr(node, property_name, value)
                    success = True
                    print(f"✅ Updated via setattr: {property_name} = {value}")
                except Exception as e:
                    print(f"setattr failed: {e}")
            
            if success:
                self.mark_modified()
                
                # Get node name safely for logging
                try:
                    node_name = node.name() if callable(node.name) else str(node.name)
                except:
                    node_name = "Unknown Node"
                
                print(f"✅ Successfully updated {node_name}.{property_name} = {value}")
            else:
                print(f"❌ Failed to update property {property_name} with all methods")
            
        except Exception as e:
            print(f"❌ Error updating node property {property_name}: {e}")
            import traceback
            traceback.print_exc()

    def browse_file_for_property(self, line_edit, property_name):
        """Open file browser for file path properties"""
        try:
            if 'model' in property_name.lower():
                file_filter = "Model files (*.nef *.onnx *.tflite);;All files (*.*)"
                title = "Select Model File"
            elif 'input' in property_name.lower():
                file_filter = "Media files (*.mp4 *.avi *.jpg *.png *.bmp);;All files (*.*)"
                title = "Select Input File"
            else:
                file_filter = "All files (*.*)"
                title = "Select File"
            
            filename, _ = QFileDialog.getOpenFileName(self, title, "", file_filter)
            if filename:
                line_edit.setText(filename)
                
        except Exception as e:
            print(f"Error browsing file: {e}")

    def create_stage_config_panel(self):
        """Create stage configuration panel"""
        widget = QScrollArea()
        content = QWidget()
        layout = QVBoxLayout(content)
        
        # Header
        header = QLabel("Stage Configuration")
        header.setStyleSheet("color: #f9e2af; font-size: 14px; font-weight: bold; padding: 5px;")
        layout.addWidget(header)
        
        # Number of stages
        stages_layout = QHBoxLayout()
        stages_layout.addWidget(QLabel("Number of Stages:"))
        self.stages_spinbox = QSpinBox()
        self.stages_spinbox.setRange(1, 10)
        self.stages_spinbox.setValue(2)
        self.stages_spinbox.valueChanged.connect(self.update_stage_configs)
        stages_layout.addWidget(self.stages_spinbox)
        layout.addLayout(stages_layout)
        
        # Container for dynamic stage configurations
        self.stage_configs_container = QWidget()
        self.stage_configs_layout = QVBoxLayout(self.stage_configs_container)
        layout.addWidget(self.stage_configs_container)
        
        # Initialize with default stages
        self.update_stage_configs()
        
        layout.addStretch()
        widget.setWidget(content)
        widget.setWidgetResizable(True)
        return widget

    def create_performance_panel(self):
        """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("Pipeline Metrics")
        metrics_layout = QGridLayout(metrics_group)
        
        # FPS estimation
        metrics_layout.addWidget(QLabel("Estimated FPS:"), 0, 0)
        self.fps_label = QLabel("0.0")
        self.fps_label.setStyleSheet("color: #a6e3a1; font-weight: bold;")
        metrics_layout.addWidget(self.fps_label, 0, 1)
        
        # Latency estimation
        metrics_layout.addWidget(QLabel("Pipeline Latency:"), 1, 0)
        self.latency_label = QLabel("0.0 ms")
        self.latency_label.setStyleSheet("color: #fab387; font-weight: bold;")
        metrics_layout.addWidget(self.latency_label, 1, 1)
        
        # Memory usage
        metrics_layout.addWidget(QLabel("Memory Usage:"), 2, 0)
        self.memory_label = QLabel("0 MB")
        self.memory_label.setStyleSheet("color: #f38ba8; font-weight: bold;")
        metrics_layout.addWidget(self.memory_label, 2, 1)
        
        layout.addWidget(metrics_group)
        
        # Performance optimization suggestions
        suggestions_group = QGroupBox("Optimization Suggestions")
        suggestions_layout = QVBoxLayout(suggestions_group)
        
        self.suggestions_text = QTextBrowser()
        self.suggestions_text.setMaximumHeight(150)
        self.suggestions_text.setText("• Connect nodes to see performance analysis\n• Add stages to see optimization suggestions")
        suggestions_layout.addWidget(self.suggestions_text)
        
        layout.addWidget(suggestions_group)
        
        # Update button
        update_btn = QPushButton("🔄 Update Performance")
        update_btn.clicked.connect(self.update_performance_estimation)
        layout.addWidget(update_btn)
        
        layout.addStretch()
        widget.setWidget(content)
        widget.setWidgetResizable(True)
        return widget

    def create_dongle_panel(self):
        """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)
        
        # Available dongles
        available_group = QGroupBox("Available Dongles")
        available_layout = QVBoxLayout(available_group)
        
        # Auto-detect button
        detect_btn = QPushButton("🔍 Auto-Detect Dongles")
        detect_btn.clicked.connect(self.detect_dongles)
        available_layout.addWidget(detect_btn)
        
        # Dongles list
        self.dongles_list = QListWidget()
        self.dongles_list.setMaximumHeight(120)
        available_layout.addWidget(self.dongles_list)
        
        layout.addWidget(available_group)
        
        # Dongle allocation
        allocation_group = QGroupBox("Stage Allocation")
        allocation_layout = QVBoxLayout(allocation_group)
        
        # Container for allocation widgets
        self.allocation_container = QWidget()
        self.allocation_layout = QVBoxLayout(self.allocation_container)
        allocation_layout.addWidget(self.allocation_container)
        
        layout.addWidget(allocation_group)
        
        # Initialize dongle detection
        self.detect_dongles()
        
        layout.addStretch()
        widget.setWidget(content)
        widget.setWidgetResizable(True)
        return widget

    def update_stage_configs(self):
        """Update stage configuration widgets based on number of stages"""
        if not self.stage_configs_layout or not self.stages_spinbox:
            return
            
        # Clear existing configs
        for i in reversed(range(self.stage_configs_layout.count())):
            self.stage_configs_layout.itemAt(i).widget().setParent(None)
        
        # Add new stage configs
        num_stages = self.stages_spinbox.value()
        for i in range(num_stages):
            stage_group = QGroupBox(f"Stage {i+1}")
            stage_layout = QFormLayout(stage_group)
            
            # Model path
            model_edit = QLineEdit()
            model_edit.setPlaceholderText("model.nef")
            stage_layout.addRow("Model Path:", model_edit)
            
            # Port IDs
            ports_edit = QLineEdit()
            ports_edit.setPlaceholderText("28,32")
            stage_layout.addRow("Port IDs:", ports_edit)
            
            # Queue size
            queue_spin = QSpinBox()
            queue_spin.setRange(1, 100)
            queue_spin.setValue(10)
            stage_layout.addRow("Queue Size:", queue_spin)
            
            self.stage_configs_layout.addWidget(stage_group)
        
        # Update allocation panel
        self.update_allocation_panel()

    def update_allocation_panel(self):
        """Update dongle allocation panel"""
        if not self.allocation_layout or not self.stages_spinbox or not self.dongles_list:
            return
            
        # Clear existing allocations
        for i in reversed(range(self.allocation_layout.count())):
            self.allocation_layout.itemAt(i).widget().setParent(None)
        
        # Add allocation widgets for each stage
        num_stages = self.stages_spinbox.value()
        for i in range(num_stages):
            alloc_layout = QHBoxLayout()
            alloc_layout.addWidget(QLabel(f"Stage {i+1}:"))
            
            dongle_combo = QComboBox()
            dongle_combo.addItem("Auto-assign")
            # Add detected dongles
            for j in range(self.dongles_list.count()):
                dongle_combo.addItem(self.dongles_list.item(j).text())
            
            alloc_layout.addWidget(dongle_combo)
            
            alloc_widget = QWidget()
            alloc_widget.setLayout(alloc_layout)
            self.allocation_layout.addWidget(alloc_widget)

    def detect_dongles(self):
        """Simulate dongle detection"""
        if not self.dongles_list:
            return
            
        self.dongles_list.clear()
        # Simulate detected dongles
        dongles = ["KL520 Dongle (Port 28)", "KL520 Dongle (Port 32)", "KL720 Dongle (Port 36)"]
        for dongle in dongles:
            self.dongles_list.addItem(f"🔌 {dongle}")

    def update_performance_estimation(self):
        """Update performance metrics based on current pipeline"""
        if not all([self.fps_label, self.latency_label, self.memory_label, self.suggestions_text, self.stages_spinbox]):
            return
            
        # Simulate performance calculation
        num_nodes = len(self.graph.all_nodes())
        num_stages = self.stages_spinbox.value()
        
        # Simple estimation logic
        base_fps = 30.0
        fps = base_fps / max(1, num_stages * 0.5)
        latency = num_stages * 15 + num_nodes * 5
        memory = num_stages * 50 + num_nodes * 20
        
        self.fps_label.setText(f"{fps:.1f}")
        self.latency_label.setText(f"{latency:.1f} ms")
        self.memory_label.setText(f"{memory} MB")
        
        # Generate suggestions
        suggestions = []
        if num_stages > 3:
            suggestions.append("• Consider reducing stages for better performance")
        if num_nodes > 5:
            suggestions.append("• Complex pipelines may benefit from optimization")
        if fps < 15:
            suggestions.append("• Low FPS detected - check dongle allocation")
        
        if not suggestions:
            suggestions = ["• Pipeline looks optimized!", "• Consider adding more stages for complex workflows"]
        
        self.suggestions_text.setText("\n".join(suggestions))

    def update_window_title(self):
        title = f"Pipeline Dashboard - {self.project_name or 'Untitled'}"
        if self.current_file:
            title += f" [{os.path.basename(self.current_file)}]"
        if self.is_modified:
            title += "*"
        self.setWindowTitle(title)

    def mark_modified(self, *args, **kwargs):
        if not self.is_modified:
            self.is_modified = True
            self.update_window_title()

    def mark_saved(self):
        if self.is_modified:
            self.is_modified = False
            self.update_window_title()

    def on_port_connected(self, *args, **kwargs):
        """Handle port connection - update performance automatically"""
        self.update_performance_estimation()

    def on_port_disconnected(self, *args, **kwargs):
        """Handle port disconnection - update performance automatically"""
        self.update_performance_estimation()

    def delete_selected_nodes(self):
        """Delete selected nodes"""
        selected_nodes = self.graph.selected_nodes()
        for node in selected_nodes:
            self.graph.delete_node(node)

    def setup_menu(self):
        """Setup menu bar"""
        menubar = self.menuBar()
        
        # File menu
        file_menu = menubar.addMenu('File')
        
        save_action = QAction('Save', self)
        save_action.setShortcut('Ctrl+S')
        save_action.triggered.connect(self.save_pipeline)
        file_menu.addAction(save_action)
        
        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_action = QAction('Export Configuration', self)
        export_action.triggered.connect(self.export_configuration)
        file_menu.addAction(export_action)

    def save_pipeline(self):
        """Save pipeline to current file"""
        if self.current_file:
            self.save_to_file(self.current_file)
        else:
            self.save_pipeline_as()

    def save_pipeline_as(self):
        """Save pipeline with file dialog"""
        filename, _ = QFileDialog.getSaveFileName(
            self, "Save Pipeline As", f"{self.project_name}.mflow", 
            "MFlow files (*.mflow);;All files (*.*)")
        
        if filename:
            self.current_file = filename
            self.save_to_file(filename)

    def save_to_file(self, filename):
        """Save pipeline data to file"""
        try:
            performance_metrics = {}
            if all([self.fps_label, self.latency_label, self.memory_label]):
                performance_metrics = {
                    "fps": self.fps_label.text(),
                    "latency": self.latency_label.text(),
                    "memory": self.memory_label.text()
                }
            
            pipeline_data = {
                "project_name": self.project_name,
                "description": self.description,
                "graph_data": self.graph.serialize_session(),
                "stage_configs": self.get_stage_configs(),
                "performance_metrics": performance_metrics,
                "metadata": {"version": "1.0", "editor": "IntegratedDashboard"}
            }
            
            with open(filename, 'w') as f:
                json.dump(pipeline_data, f, indent=2)
            
            self.mark_saved()
            QMessageBox.information(self, "Success", f"Pipeline saved to {os.path.basename(filename)}")
            
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to save pipeline: {str(e)}")

    def get_stage_configs(self):
        """Extract stage configurations from UI"""
        if not self.stage_configs_layout:
            return []
            
        configs = []
        for i in range(self.stage_configs_layout.count()):
            widget = self.stage_configs_layout.itemAt(i).widget()
            if isinstance(widget, QGroupBox):
                layout = widget.layout()
                config = {}
                for j in range(0, layout.rowCount()):
                    label_item = layout.itemAt(j, QFormLayout.LabelRole)
                    field_item = layout.itemAt(j, QFormLayout.FieldRole)
                    if label_item and field_item:
                        label = label_item.widget().text().replace(":", "")
                        field = field_item.widget()
                        if isinstance(field, QLineEdit):
                            config[label.lower().replace(" ", "_")] = field.text()
                        elif isinstance(field, QSpinBox):
                            config[label.lower().replace(" ", "_")] = field.value()
                configs.append(config)
        return configs

    def load_pipeline_file(self, filename):
        """Load pipeline from file"""
        try:
            with open(filename, 'r') as f:
                data = json.load(f)
            
            # Load graph data
            if "graph_data" in data:
                self.graph.deserialize_session(data["graph_data"])
            
            # Load stage configs if available
            if "stage_configs" in data:
                self.load_stage_configs(data["stage_configs"])
            
            # Load performance metrics if available
            if "performance_metrics" in data:
                metrics = data["performance_metrics"]
                self.fps_label.setText(metrics.get("fps", "0.0"))
                self.latency_label.setText(metrics.get("latency", "0.0 ms"))
                self.memory_label.setText(metrics.get("memory", "0 MB"))
            
            self.mark_saved()
            self.update_performance_estimation()
            
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to load pipeline: {str(e)}")

    def load_stage_configs(self, configs):
        """Load stage configurations into UI"""
        if not self.stages_spinbox or not self.stage_configs_layout or not configs:
            return
            
        self.stages_spinbox.setValue(len(configs))
        # The update_stage_configs will be called automatically
        # Then we can populate the fields
        for i, config in enumerate(configs):
            if i < self.stage_configs_layout.count():
                widget = self.stage_configs_layout.itemAt(i).widget()
                if isinstance(widget, QGroupBox):
                    layout = widget.layout()
                    for j in range(0, layout.rowCount()):
                        label_item = layout.itemAt(j, QFormLayout.LabelRole)
                        field_item = layout.itemAt(j, QFormLayout.FieldRole)
                        if label_item and field_item:
                            label = label_item.widget().text().replace(":", "")
                            field = field_item.widget()
                            key = label.lower().replace(" ", "_")
                            if key in config:
                                if isinstance(field, QLineEdit):
                                    field.setText(str(config[key]))
                                elif isinstance(field, QSpinBox):
                                    field.setValue(int(config[key]))

    def export_configuration(self):
        """Export pipeline configuration for deployment"""
        try:
            dongles = []
            if self.dongles_list:
                dongles = [self.dongles_list.item(i).text() for i in range(self.dongles_list.count())]
            
            performance_estimate = {}
            if all([self.fps_label, self.latency_label, self.memory_label]):
                performance_estimate = {
                    "fps": self.fps_label.text(),
                    "latency": self.latency_label.text(),
                    "memory": self.memory_label.text()
                }
            
            config_data = {
                "pipeline_name": self.project_name,
                "stages": self.get_stage_configs(),
                "performance_estimate": performance_estimate,
                "dongles": dongles,
                "export_timestamp": json.dumps({"timestamp": "2024-01-01T00:00:00Z"})
            }
            
            filename, _ = QFileDialog.getSaveFileName(
                self, "Export Configuration", f"{self.project_name}_config.json", 
                "JSON files (*.json);;All files (*.*)")
            
            if filename:
                with open(filename, 'w') as f:
                    json.dump(config_data, f, indent=2)
                
                QMessageBox.information(self, "Success", f"Configuration exported to {os.path.basename(filename)}")
                
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to export configuration: {str(e)}")

class PipelineEditor(QMainWindow):
    """Main pipeline editor using NodeGraphQt"""
    
    def __init__(self, project_name="", description="", filename=None):
        super().__init__()
        self.project_name = project_name
        self.description = description
        self.current_file = filename
        self.is_modified = False
        
        # Initialize node graph
        self.graph = NodeGraph(properties_bin_class=None)
        
        # Register custom nodes
        nodes_to_register = [InputNode, ModelNode, PreprocessNode, PostprocessNode, OutputNode]
        
        for node_class in nodes_to_register:
            try:
                self.graph.register_node(node_class)
            except Exception as e:
                print(f"Failed to register {node_class.__name__}: {e}")
        
        # --- MODIFICATION START ---
        # Create properties bin widget to be docked in the main window.
        try:
            # 完全禁用原来的PropertiesBinWidget，使用我们的自定义面板
            print("Creating CustomPropertiesWidget...")  # 调试信息
            self.properties_bin = CustomPropertiesWidget(self.graph)
            self.properties_bin.setMinimumWidth(300)
            print("CustomPropertiesWidget created successfully")  # 调试信息
        except Exception as e:
            print(f"Failed to create CustomPropertiesWidget: {e}")
            import traceback
            traceback.print_exc()
            self.properties_bin = None
        # --- MODIFICATION END ---
            
        # Connect signals
        # We no longer need to connect node_double_clicked to show properties.
        # The PropertiesBinWidget handles selection changes automatically.
        self.graph.node_created.connect(self.mark_modified) # type: ignore
        self.graph.nodes_deleted.connect(self.mark_modified) # type: ignore
        self.graph.property_changed.connect(self.mark_modified) # type: ignore
        
        if hasattr(self.graph, 'port_connected'):
            self.graph.port_connected.connect(self.on_port_connected)
        if hasattr(self.graph, 'port_disconnected'):
            self.graph.port_disconnected.connect(self.on_port_disconnected)
        
        self.setup_ui()
        self.setup_menu()
        
        # Add keyboard shortcut for delete
        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)
        
        self.update_window_title()
        self.setGeometry(100, 100, 1600, 900) # Increased width for the new panel
        self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET)

    def update_window_title(self):
        title = f"Pipeline Editor - {self.project_name or 'Untitled'}"
        if self.current_file:
            title += f" [{os.path.basename(self.current_file)}]"
        if self.is_modified:
            title += "*"
        self.setWindowTitle(title)

    def mark_modified(self, *args, **kwargs):
        if not self.is_modified:
            self.is_modified = True
            self.update_window_title()

    def mark_saved(self):
        if self.is_modified:
            self.is_modified = False
            self.update_window_title()
    
    # This method is no longer needed as the primary way to show properties.
    # Kept for the fallback dialog.
    def display_properties_bin(self, node):
        if not self.properties_bin:
            self.show_simple_properties_dialog(node)
        # If the properties bin exists, it updates automatically.
        # Double-clicking can still be used to ensure it's visible.
        elif self.properties_bin and not self.properties_bin.isVisible():
             self.properties_bin.show()

    def show_simple_properties_dialog(self, node):
        try:
            dialog = SimplePropertiesDialog(node, self)
            if dialog.exec_() == QDialog.Accepted:
                self.mark_modified()
                
        except Exception as e:
            print(f"Error with interactive SimplePropertiesDialog for {node.name()}: {e}")
            QMessageBox.warning(self, "Properties Error", 
                                  f"Could not display interactive properties for {node.name()}: {str(e)}")
    
    def on_port_connected(self, input_port, output_port):
        self.mark_modified()
    
    def on_port_disconnected(self, input_port, output_port):
        self.mark_modified()
    
    def show_connections(self):
        all_nodes = self.graph.all_nodes()
        connections = []
        
        for node in all_nodes:
            for output_port in node.output_ports():
                for connected_input_port in output_port.connected_ports():
                    connections.append(f"{node.name()}.{output_port.name()} → {connected_input_port.node().name()}.{connected_input_port.name()}")
        
        QMessageBox.information(self, "Graph Connections", "Current connections:\n" + "\n".join(connections) if connections else "No connections found.")
    
    def show_registered_nodes(self):
        try:
            registered_info = []
            if hasattr(self.graph, 'all_registered_nodes'):
                nodes_dict = self.graph.all_registered_nodes()
                registered_info.append(f"all_registered_nodes(): {list(nodes_dict.keys())}")
            
            msg = "Registered node information:\n\n" + "\n\n".join(registered_info)
            QMessageBox.information(self, "Registered Nodes Debug", msg)
            
        except Exception as e:
            QMessageBox.critical(self, "Debug Error", f"Error getting registered nodes: {e}")
    
    def setup_ui(self):
        """Setup main UI with a docked properties panel."""
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        layout = QHBoxLayout(central_widget)
        
        # --- MODIFICATION START ---
        # The main splitter will now hold three widgets: Palette, Graph, Properties.
        main_splitter = QSplitter(Qt.Horizontal)
        
        # 1. Left Panel: Node Palette
        self.palette = NodePalette(self.graph)
        self.palette.setMaximumWidth(250)
        main_splitter.addWidget(self.palette)
        
        # 2. Center Panel: Node Graph
        graph_widget = self.graph.widget
        graph_widget.setFocusPolicy(Qt.StrongFocus)
        main_splitter.addWidget(graph_widget)
        
        # 3. Right Panel: Properties Bin
        if self.properties_bin:
            main_splitter.addWidget(self.properties_bin)
            # Set initial sizes for the three panels
            main_splitter.setSizes([220, 1080, 300])
        else:
            # Fallback if properties bin failed to create
            main_splitter.setSizes([250, 1350])
        # --- MODIFICATION END ---
            
        layout.addWidget(main_splitter)
        graph_widget.setFocus()
    
    def setup_menu(self):
        menubar = self.menuBar()
        
        file_menu = menubar.addMenu('File')
        # ... (File menu remains the same)
        save_action = file_menu.addAction('Save')
        save_action.setShortcut('Ctrl+S')
        save_action.triggered.connect(self.save_pipeline)
        save_as_action = file_menu.addAction('Save As...')
        save_as_action.triggered.connect(self.save_pipeline_as)
        open_action = file_menu.addAction('Open...')
        open_action.setShortcut('Ctrl+O')
        open_action.triggered.connect(self.open_pipeline)
        file_menu.addSeparator()
        close_action = file_menu.addAction('Close')
        close_action.triggered.connect(self.close)

        edit_menu = menubar.addMenu('Edit')
        # ... (Edit menu remains the same)
        undo_action = edit_menu.addAction('Undo')
        undo_action.setShortcut('Ctrl+Z')
        undo_action.triggered.connect(self.graph.undo_stack().undo)
        redo_action = edit_menu.addAction('Redo')
        redo_action.setShortcut('Ctrl+Y')
        redo_action.triggered.connect(self.graph.undo_stack().redo)
        edit_menu.addSeparator()
        delete_action = edit_menu.addAction('Delete Selected')
        delete_action.setShortcut('Delete')
        delete_action.triggered.connect(self.delete_selected_nodes)
        self.addAction(delete_action)
        select_all_action = edit_menu.addAction('Select All')
        select_all_action.setShortcut('Ctrl+A')
        select_all_action.triggered.connect(self.graph.select_all)
        clear_selection_action = edit_menu.addAction('Clear Selection')
        clear_selection_action.setShortcut('Ctrl+D')
        clear_selection_action.triggered.connect(self.graph.clear_selection)
        
        view_menu = menubar.addMenu('View')
        
        # --- MODIFICATION START ---
        # Change the action to be a checkable toggle for the properties panel.
        if self.properties_bin:
            properties_action = view_menu.addAction('Toggle Properties Panel')
            properties_action.setCheckable(True)
            properties_action.setChecked(True) # Start with the panel visible.
            properties_action.triggered.connect(self.toggle_properties_panel)
        # --- MODIFICATION END ---
        
        # Pipeline menu
        pipeline_menu = menubar.addMenu('Pipeline')
        configure_stages_action = pipeline_menu.addAction('Configure Stages')
        configure_stages_action.triggered.connect(self.show_stage_configuration_from_editor)
        performance_action = pipeline_menu.addAction('Performance Analysis')
        performance_action.triggered.connect(self.show_performance_analysis)
        deploy_action = pipeline_menu.addAction('Deploy Pipeline')
        deploy_action.triggered.connect(self.show_deploy_dialog)
            
        view_menu.addSeparator()
        fit_action = view_menu.addAction('Fit to Selection')
        fit_action.setShortcut('F')
        fit_action.triggered.connect(self.graph.fit_to_selection)
        auto_layout_action = view_menu.addAction('Auto Layout All')
        auto_layout_action.triggered.connect(self.graph.auto_layout_nodes) # type: ignore
        
        debug_menu = menubar.addMenu('Debug')
        # ... (Debug menu remains the same)
        show_connections_action = debug_menu.addAction('Show All Connections')
        show_connections_action.triggered.connect(self.show_connections)
        show_registered_action = debug_menu.addAction('Show Registered Nodes')
        show_registered_action.triggered.connect(self.show_registered_nodes)

    # --- NEW METHOD START ---
    def toggle_properties_panel(self, checked):
        """Show or hide the properties panel."""
        if self.properties_bin:
            self.properties_bin.setVisible(checked)
    # --- NEW METHOD END ---

    # This method is now replaced by toggle_properties_panel
    def show_properties_panel(self):
        """Show properties panel safely (Legacy, now used for fallback)."""
        if self.properties_bin:
            if not self.properties_bin.isVisible():
                self.properties_bin.show()
            self.properties_bin.raise_()
            self.properties_bin.activateWindow()
        else:
            selected_nodes = self.graph.selected_nodes()
            if selected_nodes:
                self.show_simple_properties_dialog(selected_nodes[0])
            else:
                QMessageBox.information(self, "Properties Panel", "Properties panel is not available.")
    
    def delete_selected_nodes(self):
        selected_nodes = self.graph.selected_nodes()
        if selected_nodes:
            try:
                self.graph.delete_nodes(selected_nodes)
            except Exception as e:
                print(f"Error deleting nodes: {e}")
                QMessageBox.warning(self, "Delete Error", f"Failed to delete nodes: {e}")
    
    def save_pipeline(self):
        if self.current_file:
            return self._save_to_file(self.current_file)
        else:
            return self.save_pipeline_as()
    
    def save_pipeline_as(self):
        default_name = f"{self.project_name or 'untitled'}.mflow"
        filename, _ = QFileDialog.getSaveFileName(
            self, "Save Pipeline", default_name, 
            "MFlow files (*.mflow);;All files (*.*)")
        
        if filename:
            if self._save_to_file(filename):
                self.current_file = filename
                self.project_name = os.path.splitext(os.path.basename(filename))[0]
                self.mark_saved()
                return True
        return False
            
    def _save_to_file(self, filename):
        try:
            graph_data = self.graph.serialize_session()
            pipeline_data = {
                "project_name": self.project_name,
                "description": self.description,
                "graph_data": graph_data,
                "metadata": { "version": "1.0", "editor": "NodeGraphQt" }
            }
            with open(filename, 'w') as f:
                json.dump(pipeline_data, f, indent=2)
            
            self.mark_saved()
            QMessageBox.information(self, "Saved", f"Pipeline saved to {os.path.basename(filename)}")
            return True
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to save to {filename}: {str(e)}")
            return False
    
    def open_pipeline(self):
        if self.is_modified:
            reply = QMessageBox.question(self, 'Save Changes',
                                         "Current pipeline has unsaved changes. Save before opening a new one?",
                                         QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
                                         QMessageBox.Save)
            if reply == QMessageBox.Save:
                if not self.save_pipeline():
                    return
            elif reply == QMessageBox.Cancel:
                return

        filename, _ = QFileDialog.getOpenFileName(
            self, "Open Pipeline", "", 
            "MFlow files (*.mflow);;All files (*.*)")
        
        if filename:
            self.load_pipeline_file(filename)
    
    def load_pipeline_file(self, filename):
        try:
            with open(filename, 'r') as f:
                data = json.load(f)
            
            self.project_name = data.get("project_name", os.path.splitext(os.path.basename(filename))[0])
            self.description = data.get("description", "")
            
            self.graph.clear_session()
            if "graph_data" in data:
                self.graph.deserialize_session(data["graph_data"])
            
            self.current_file = filename
            self.mark_saved()
            
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to open {filename}: {str(e)}")
            self.project_name = "Untitled"
            self.description = ""
            self.current_file = None
            self.graph.clear_session()
            self.mark_saved()


    def show_stage_configuration_from_editor(self):
        """Show stage configuration from editor"""
        # Get current pipeline data
        current_data = {
            "graph_data": self.graph.serialize_session(),
            "project_name": self.project_name,
            "description": self.description
        }
        
        config_dialog = StageConfigurationDialog(current_data, self)
        config_dialog.exec_()
    
    def show_performance_analysis(self):
        """Show performance analysis for current pipeline"""
        # Generate sample stage configs from current pipeline
        stage_configs = [
            {"name": "Input Processing", "dongles": 2, "port_ids": "28,30", "model_path": ""},
            {"name": "Main Inference", "dongles": 4, "port_ids": "32,34,36,38", "model_path": ""},
            {"name": "Post Processing", "dongles": 2, "port_ids": "40,42", "model_path": ""}
        ]
        
        perf_panel = PerformanceEstimationPanel(stage_configs, self)
        perf_panel.exec_()
    
    def show_deploy_dialog(self):
        """Show deployment dialog for current pipeline"""
        # Generate sample stage configs from current pipeline
        stage_configs = [
            {"name": "Input Processing", "dongles": 2, "port_ids": "28,30", "model_path": ""},
            {"name": "Main Inference", "dongles": 4, "port_ids": "32,34,36,38", "model_path": ""},
            {"name": "Post Processing", "dongles": 2, "port_ids": "40,42", "model_path": ""}
        ]
        
        deploy_dialog = SaveDeployDialog(stage_configs, self)
        deploy_dialog.exec_()

    def closeEvent(self, event):
        if self.is_modified:
            reply = QMessageBox.question(self, 'Save Changes',
                                         "The pipeline has unsaved changes. Do you want to save them?",
                                         QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
                                         QMessageBox.Save)

            if reply == QMessageBox.Save:
                if self.save_pipeline():
                    event.accept()
                else:
                    event.ignore()
            elif reply == QMessageBox.Discard:
                event.accept()
            else:
                event.ignore()
        else:
            event.accept()
            

class DashboardLogin(QWidget):
    def __init__(self):
        super().__init__()
        self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET)
        self.pipeline_editor = None # Keep track of the editor window
        self.recent_files = self.load_recent_files()
        # Store current project settings
        self.current_project_name = ""
        self.current_description = ""
        self.current_pipeline_data = {}
        self.initUI()

    def initUI(self):
        self.setWindowTitle("Cluster Dashboard - ML Pipeline Builder")
        self.setGeometry(300, 300, 700, 500)

        # Create main scroll area to ensure all content is viewable
        scrollArea = QScrollArea(self)
        scrollContent = QWidget()
        scrollArea.setWidget(scrollContent)
        scrollArea.setWidgetResizable(True)
        scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)

        mainLayout = QVBoxLayout(scrollContent)
        mainLayout.setContentsMargins(40, 40, 40, 40)
        mainLayout.setSpacing(25)

        # Header section with title and subtitle
        headerLayout = QVBoxLayout()
        headerLayout.setSpacing(8)
        
        titleLabel = QLabel("Cluster")
        titleFont = QFont("Segoe UI", 32, QFont.Bold)
        titleLabel.setFont(titleFont)
        titleLabel.setAlignment(Qt.AlignCenter)
        titleLabel.setStyleSheet("color: #89b4fa; margin-bottom: 5px;")
        
        subtitleLabel = QLabel("AI Pipeline Builder & Execution Platform")
        subtitleFont = QFont("Inter", 14)
        subtitleLabel.setFont(subtitleFont)
        subtitleLabel.setAlignment(Qt.AlignCenter)
        subtitleLabel.setStyleSheet("color: #a6adc8; margin-bottom: 20px;")
        
        headerLayout.addWidget(titleLabel)
        headerLayout.addWidget(subtitleLabel)

        # Action buttons section
        buttonLayout = QVBoxLayout()
        buttonLayout.setSpacing(15)

        # Create new pipeline button (primary action)
        self.createNewPipelineButton = QPushButton("🚀 Create New Pipeline")
        self.createNewPipelineButton.setMinimumHeight(55)
        self.createNewPipelineButton.clicked.connect(self.create_new_pipeline_dialog)
        self.createNewPipelineButton.setStyleSheet("""
            QPushButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #89b4fa, stop:1 #74c7ec);
                color: #1e1e2e;
                border: none;
                padding: 15px 25px;
                border-radius: 12px;
                font-weight: 700;
                font-size: 16px;
                text-align: left;
            }
            QPushButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #a6c8ff, stop:1 #89dceb);
                transform: translateY(-2px);
            }
        """)

        # Edit existing pipeline button
        self.editPipelineButton = QPushButton("📁 Edit Previous Pipeline")
        self.editPipelineButton.setMinimumHeight(55)
        self.editPipelineButton.clicked.connect(self.edit_previous_pipeline)
        self.editPipelineButton.setStyleSheet("""
            QPushButton {
                background-color: #313244;
                color: #cdd6f4;
                border: 2px solid #89b4fa;
                padding: 15px 25px;
                border-radius: 12px;
                font-weight: 600;
                font-size: 16px;
                text-align: left;
            }
            QPushButton:hover {
                background-color: #383a59;
                border-color: #a6c8ff;
                color: #ffffff;
            }
        """)

        # Recent files section
        recentLabel = QLabel("Recent Pipelines")
        recentLabel.setFont(QFont("Segoe UI", 12, QFont.Bold))
        recentLabel.setStyleSheet("color: #f9e2af; margin-top: 10px;")
        
        self.recentFilesList = QListWidget()
        self.recentFilesList.setMaximumHeight(120)
        self.recentFilesList.setStyleSheet("""
            QListWidget {
                background-color: #313244;
                border: 2px solid #45475a;
                border-radius: 12px;
                padding: 8px;
            }
            QListWidget::item {
                padding: 10px 14px;
                border-radius: 6px;
                margin: 2px;
                color: #cdd6f4;
            }
            QListWidget::item:hover {
                background-color: #383a59;
            }
            QListWidget::item:selected {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #89b4fa, stop:1 #74c7ec);
                color: #1e1e2e;
            }
        """)
        self.populate_recent_files()
        self.recentFilesList.itemDoubleClicked.connect(self.open_recent_file)

        # Help/Info section
        infoLabel = QLabel("💡 Quick Start Guide")
        infoLabel.setFont(QFont("Segoe UI", 12, QFont.Bold))
        infoLabel.setStyleSheet("color: #f9e2af; margin-top: 15px;")
        
        helpText = QLabel(
            "• Create a new pipeline to build ML workflows visually\n"
            "• Edit existing .mflow files to continue work\n"
            "• Drag and connect nodes to create processing chains\n"
            "• Configure dongle properties in the integrated panel\n"
            "• Monitor performance and deploy your pipelines"
        )
        helpText.setStyleSheet("""
            color: #a6adc8;
            font-size: 12px;
            line-height: 1.6;
            padding: 16px;
            background-color: #313244;
            border-radius: 12px;
            border-left: 4px solid #89b4fa;
        """)
        helpText.setWordWrap(True)
        
        # Add all sections to main layout
        buttonLayout.addWidget(self.createNewPipelineButton)
        buttonLayout.addWidget(self.editPipelineButton)
        
        mainLayout.addLayout(headerLayout)
        mainLayout.addSpacing(20)
        mainLayout.addLayout(buttonLayout)
        mainLayout.addSpacing(15)
        mainLayout.addWidget(recentLabel)
        mainLayout.addWidget(self.recentFilesList)
        mainLayout.addSpacing(10)
        mainLayout.addWidget(infoLabel)
        mainLayout.addWidget(helpText)
        mainLayout.addStretch(1)

        # Set the scroll area as the main widget
        containerLayout = QVBoxLayout(self)
        containerLayout.setContentsMargins(0, 0, 0, 0)
        containerLayout.addWidget(scrollArea)

    def load_recent_files(self):
        """Load recent files from settings or return empty list"""
        try:
            recent_files_path = os.path.expanduser("~/.cluster_recent_files.json")
            if os.path.exists(recent_files_path):
                with open(recent_files_path, 'r') as f:
                    return json.load(f)
        except Exception as e:
            print(f"Error loading recent files: {e}")
        return []

    def save_recent_files(self):
        """Save recent files to settings"""
        try:
            recent_files_path = os.path.expanduser("~/.cluster_recent_files.json")
            with open(recent_files_path, 'w') as f:
                json.dump(self.recent_files, f, indent=2)
        except Exception as e:
            print(f"Error saving recent files: {e}")

    def add_recent_file(self, filepath):
        """Add file to recent files list"""
        if filepath in self.recent_files:
            self.recent_files.remove(filepath)
        self.recent_files.insert(0, filepath)
        # Keep only last 5 files
        self.recent_files = self.recent_files[:5]
        self.save_recent_files()
        self.populate_recent_files()

    def populate_recent_files(self):
        """Populate the recent files list widget"""
        self.recentFilesList.clear()
        for filepath in self.recent_files:
            if os.path.exists(filepath):
                filename = os.path.basename(filepath)
                self.recentFilesList.addItem(f"📄 {filename}")
            else:
                # Remove non-existent files
                self.recent_files.remove(filepath)
        
        if not self.recentFilesList.count():
            self.recentFilesList.addItem("No recent files")

    def open_recent_file(self, item):
        """Open a recent file when double-clicked"""
        if item.text() == "No recent files":
            return
        
        filename = item.text().replace("📄 ", "")
        for filepath in self.recent_files:
            if os.path.basename(filepath) == filename:
                try:
                    with open(filepath, 'r') as f:
                        data = json.load(f)
                    project_name = data.get("project_name", os.path.splitext(filename)[0])
                    description = data.get("description", "")
                    
                    # Store loaded pipeline data in dashboard
                    self.current_project_name = project_name
                    self.current_description = description
                    self.current_pipeline_data = data
                    
                    self._open_editor_window(project_name, description, filepath)
                    break
                except Exception as e:
                    QMessageBox.critical(self, "Error", f"Failed to open {filename}: {str(e)}")

    def _open_editor_window(self, project_name, description, filename):
        """Helper to ensure only one editor is managed by dashboard, or handle existing."""
        if self.pipeline_editor and self.pipeline_editor.isVisible():
            # Ask if user wants to close current editor or if it's okay
            # For simplicity here, we just bring it to front.
            # A more robust solution might involve managing multiple editors or prompting user.
            self.pipeline_editor.raise_()
            self.pipeline_editor.activateWindow()
            QMessageBox.information(self, "Editor Open", "An editor window is already open.")
            # Optionally, load the new data into the existing editor if desired, after prompting.
            return

        # Create the integrated dashboard instead of separate pipeline editor
        self.pipeline_editor = IntegratedPipelineDashboard(project_name, description, filename)
        if filename and os.path.exists(filename): # If filename is provided and exists, load it
             self.pipeline_editor.load_pipeline_file(filename)
        self.pipeline_editor.show()


    def edit_previous_pipeline(self):
        """Open existing pipeline"""
        filename, _ = QFileDialog.getOpenFileName(
            self, "Open Pipeline", "", 
            "MFlow files (*.mflow);;All files (*.*)")
        
        if filename:
            try:
                # Basic check, more validation could be in PipelineEditor.load_pipeline_file
                with open(filename, 'r') as f:
                    data = json.load(f) 
                project_name = data.get("project_name", os.path.splitext(os.path.basename(filename))[0])
                description = data.get("description", "")
                
                # Store loaded pipeline data in dashboard
                self.current_project_name = project_name
                self.current_description = description
                self.current_pipeline_data = data
                
                self._open_editor_window(project_name, description, filename)
                self.add_recent_file(filename)
                
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to open pipeline {filename}: {str(e)}")
    
    def show_stage_configuration(self):
        """Show stage configuration dialog for quick pipeline setup"""
        # Use current project data if available, otherwise create minimal pipeline
        if self.current_pipeline_data:
            pipeline_data = self.current_pipeline_data.copy()
        else:
            # Create a minimal pipeline with current project settings
            pipeline_data = {
                "project_name": self.current_project_name or "Untitled Pipeline",
                "description": self.current_description or "Pipeline configuration",
                "graph_data": {
                    "nodes": {
                        "node1": {"type_": "ModelNode", "custom": {"model_path": ""}},
                        "node2": {"type_": "ModelNode", "custom": {"model_path": ""}}
                    }
                },
                "metadata": {"version": "1.0", "editor": "Dashboard"}
            }
        
        config_dialog = StageConfigurationDialog(pipeline_data, self)
        if config_dialog.exec_():
            # Update stored pipeline data with stage configurations
            self.current_pipeline_data = pipeline_data
            self.current_pipeline_data["stage_configs"] = config_dialog.get_stage_configs()

    def create_new_pipeline_dialog(self):
        """Create new pipeline"""
        dialog = CreatePipelineDialog(self)
        if dialog.exec_():
            data = dialog.get_data()
            
            # Store project settings in dashboard
            self.current_project_name = data['project_name']
            self.current_description = data['description']
            
            # Prompt for initial save location
            save_filename, _ = QFileDialog.getSaveFileName(
                self, "Save New Pipeline As", f"{data['project_name']}.mflow", 
                "MFlow files (*.mflow);;All files (*.*)")
            
            if save_filename:
                try:
                    # Create an empty pipeline structure to save
                    empty_graph_data = NodeGraph().serialize_session() # Get empty session data
                    pipeline_data = {
                        "project_name": data['project_name'],
                        "description": data['description'],
                        "graph_data": empty_graph_data, # Start with an empty graph
                        "metadata": { "version": "1.0", "editor": "NodeGraphQt" }
                    }
                    
                    # Store current pipeline data in dashboard
                    self.current_pipeline_data = pipeline_data
                    
                    with open(save_filename, 'w') as f:
                        json.dump(pipeline_data, f, indent=2)
                    
                    QMessageBox.information(
                        self, "Pipeline Created", 
                        f"New pipeline '{data['project_name']}' has been created and saved as {os.path.basename(save_filename)}."
                    )
                    
                    # Open editor with this new, saved pipeline
                    self._open_editor_window(data['project_name'], data['description'], save_filename)
                    self.add_recent_file(save_filename)
                    
                except Exception as e:
                    QMessageBox.critical(self, "Error", f"Failed to create and save new pipeline: {str(e)}")


class StageConfigurationDialog(QDialog):
    """Dialog for configuring pipeline stages and dongle allocation"""
    
    def __init__(self, pipeline_data=None, parent=None):
        super().__init__(parent)
        self.pipeline_data = pipeline_data or {}
        self.stage_configs = []
        self.total_dongles_available = 16  # Default max dongles
        self.setWindowTitle("Stage Configuration & Dongle Allocation")
        self.setMinimumSize(800, 600)
        self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET)
        self.setup_ui()
        self.load_stages_from_pipeline()
    
    def setup_ui(self):
        layout = QVBoxLayout(self)
        
        # Header with project information
        project_name = self.pipeline_data.get("project_name", "Untitled Pipeline")
        header = QLabel(f"Configure Pipeline Stages - {project_name}")
        header.setFont(QFont("Arial", 16, QFont.Bold))
        header.setStyleSheet("color: #89b4fa; margin-bottom: 5px;")
        layout.addWidget(header)
        
        # Project description if available
        description = self.pipeline_data.get("description", "")
        if description:
            desc_label = QLabel(f"Description: {description}")
            desc_label.setStyleSheet("color: #a6adc8; margin-bottom: 10px; font-style: italic;")
            desc_label.setWordWrap(True)
            layout.addWidget(desc_label)
        
        # Instructions
        instructions = QLabel(
            "Break your pipeline into sections (A → B → C...) and assign dongles for optimal performance"
        )
        instructions.setStyleSheet("color: #a6adc8; margin-bottom: 15px;")
        instructions.setWordWrap(True)
        layout.addWidget(instructions)
        
        # Main content area
        content_splitter = QSplitter(Qt.Horizontal)
        
        # Left side: Stage list and controls
        left_widget = QWidget()
        left_layout = QVBoxLayout(left_widget)
        
        # Stage list
        stages_group = QGroupBox("Pipeline Stages")
        stages_layout = QVBoxLayout(stages_group)
        
        self.stages_list = QListWidget()
        self.stages_list.currentItemChanged.connect(self.on_stage_selected)
        stages_layout.addWidget(self.stages_list)
        
        # Stage controls
        stage_controls = QHBoxLayout()
        self.add_stage_btn = QPushButton("➕ Add Stage")
        self.add_stage_btn.clicked.connect(self.add_stage)
        self.remove_stage_btn = QPushButton("➖ Remove Stage")
        self.remove_stage_btn.clicked.connect(self.remove_stage)
        self.auto_balance_btn = QPushButton("⚖️ Auto-Balance")
        self.auto_balance_btn.clicked.connect(self.auto_balance_dongles)
        
        stage_controls.addWidget(self.add_stage_btn)
        stage_controls.addWidget(self.remove_stage_btn)
        stage_controls.addWidget(self.auto_balance_btn)
        stages_layout.addLayout(stage_controls)
        
        left_layout.addWidget(stages_group)
        
        # Right side: Stage configuration
        right_widget = QWidget()
        right_layout = QVBoxLayout(right_widget)
        
        # Stage details
        details_group = QGroupBox("Stage Configuration")
        details_layout = QFormLayout(details_group)
        
        self.stage_name_input = QLineEdit()
        self.stage_name_input.textChanged.connect(self.update_current_stage)
        details_layout.addRow("Stage Name:", self.stage_name_input)
        
        # Dongle allocation
        dongle_layout = QHBoxLayout()
        self.dongle_count_slider = QSlider(Qt.Horizontal)
        self.dongle_count_slider.setRange(1, 8)
        self.dongle_count_slider.setValue(2)
        self.dongle_count_slider.valueChanged.connect(self.update_dongle_count)
        
        self.dongle_count_label = QLabel("2 dongles")
        dongle_layout.addWidget(self.dongle_count_slider)
        dongle_layout.addWidget(self.dongle_count_label)
        details_layout.addRow("Dongles Assigned:", dongle_layout)
        
        # Port IDs
        self.port_ids_input = QLineEdit()
        self.port_ids_input.setPlaceholderText("e.g., 28,30,32 or auto")
        self.port_ids_input.textChanged.connect(self.update_current_stage)
        details_layout.addRow("Port IDs:", self.port_ids_input)
        
        # Model selection
        self.model_path_input = QLineEdit()
        model_browse_layout = QHBoxLayout()
        model_browse_btn = QPushButton("Browse")
        model_browse_btn.clicked.connect(self.browse_model_file)
        model_browse_layout.addWidget(self.model_path_input)
        model_browse_layout.addWidget(model_browse_btn)
        details_layout.addRow("Model File:", model_browse_layout)
        
        # Performance estimates
        perf_group = QGroupBox("Performance Estimation")
        perf_layout = QFormLayout(perf_group)
        
        self.estimated_fps_label = QLabel("--")
        self.estimated_latency_label = QLabel("--")
        self.throughput_label = QLabel("--")
        
        perf_layout.addRow("Estimated FPS:", self.estimated_fps_label)
        perf_layout.addRow("Latency:", self.estimated_latency_label)
        perf_layout.addRow("Throughput:", self.throughput_label)
        
        right_layout.addWidget(details_group)
        right_layout.addWidget(perf_group)
        
        # Resource summary
        resource_group = QGroupBox("Resource Summary")
        resource_layout = QFormLayout(resource_group)
        
        self.total_dongles_label = QLabel(f"0 / {self.total_dongles_available}")
        self.pipeline_fps_label = QLabel("--")
        resource_layout.addRow("Total Dongles Used:", self.total_dongles_label)
        resource_layout.addRow("Pipeline FPS:", self.pipeline_fps_label)
        
        right_layout.addWidget(resource_group)
        right_layout.addStretch()
        
        content_splitter.addWidget(left_widget)
        content_splitter.addWidget(right_widget)
        content_splitter.setSizes([400, 400])
        layout.addWidget(content_splitter)
        
        # Buttons
        button_layout = QHBoxLayout()
        self.next_btn = QPushButton("Next: Performance Estimation")
        self.next_btn.clicked.connect(self.show_performance_panel)
        self.cancel_btn = QPushButton("Cancel")
        self.cancel_btn.clicked.connect(self.reject)
        
        button_layout.addStretch()
        button_layout.addWidget(self.cancel_btn)
        button_layout.addWidget(self.next_btn)
        layout.addLayout(button_layout)
    
    def load_stages_from_pipeline(self):
        """Extract stages from pipeline data"""
        if not self.pipeline_data.get('graph_data'):
            # Add default stage if no pipeline data
            self.add_stage()
            return
        
        # Parse graph data to identify stages
        # This is a simplified implementation
        nodes = self.pipeline_data.get('graph_data', {}).get('nodes', {})
        model_nodes = [node for node in nodes.values() if 'Model' in node.get('type_', '')]
        
        if not model_nodes:
            self.add_stage()
            return
        
        for i, node in enumerate(model_nodes):
            stage_config = {
                'name': f"Stage {i+1}",
                'dongles': 2,
                'port_ids': 'auto',
                'model_path': node.get('custom', {}).get('model_path', ''),
                'node_id': node.get('id', '')
            }
            self.stage_configs.append(stage_config)
        
        self.update_stages_list()
    
    def add_stage(self):
        """Add a new stage"""
        stage_num = len(self.stage_configs) + 1
        stage_config = {
            'name': f"Stage {stage_num}",
            'dongles': 2,
            'port_ids': 'auto',
            'model_path': '',
            'node_id': ''
        }
        self.stage_configs.append(stage_config)
        self.update_stages_list()
        
        # Select the new stage
        self.stages_list.setCurrentRow(len(self.stage_configs) - 1)
    
    def remove_stage(self):
        """Remove selected stage"""
        current_row = self.stages_list.currentRow()
        if current_row >= 0 and self.stage_configs:
            self.stage_configs.pop(current_row)
            self.update_stages_list()
            
            # Select previous stage or first stage
            if self.stage_configs:
                new_row = min(current_row, len(self.stage_configs) - 1)
                self.stages_list.setCurrentRow(new_row)
    
    def update_stages_list(self):
        """Update the stages list widget"""
        self.stages_list.clear()
        for i, config in enumerate(self.stage_configs):
            dongles = config['dongles']
            item_text = f"{config['name']} ({dongles} dongles)"
            self.stages_list.addItem(item_text)
        
        self.update_resource_summary()
    
    def on_stage_selected(self, current, previous):
        """Handle stage selection"""
        row = self.stages_list.currentRow()
        if row >= 0 and row < len(self.stage_configs):
            config = self.stage_configs[row]
            
            # Update UI with selected stage data
            self.stage_name_input.setText(config['name'])
            self.dongle_count_slider.setValue(config['dongles'])
            self.port_ids_input.setText(config['port_ids'])
            self.model_path_input.setText(config['model_path'])
            
            # Update performance estimates
            self.update_performance_estimates(config)
    
    def update_current_stage(self):
        """Update current stage configuration"""
        row = self.stages_list.currentRow()
        if row >= 0 and row < len(self.stage_configs):
            config = self.stage_configs[row]
            config['name'] = self.stage_name_input.text()
            config['port_ids'] = self.port_ids_input.text()
            config['model_path'] = self.model_path_input.text()
            
            self.update_stages_list()
            self.stages_list.setCurrentRow(row)  # Maintain selection
    
    def update_dongle_count(self, value):
        """Update dongle count for current stage"""
        self.dongle_count_label.setText(f"{value} dongles")
        
        row = self.stages_list.currentRow()
        if row >= 0 and row < len(self.stage_configs):
            self.stage_configs[row]['dongles'] = value
            self.update_stages_list()
            self.stages_list.setCurrentRow(row)
            
            # Update performance estimates
            self.update_performance_estimates(self.stage_configs[row])
    
    def auto_balance_dongles(self):
        """Automatically balance dongles across stages"""
        if not self.stage_configs:
            return
        
        num_stages = len(self.stage_configs)
        dongles_per_stage = max(1, self.total_dongles_available // num_stages)
        remaining = self.total_dongles_available % num_stages
        
        for i, config in enumerate(self.stage_configs):
            config['dongles'] = dongles_per_stage + (1 if i < remaining else 0)
        
        self.update_stages_list()
        # Refresh current stage display
        current_row = self.stages_list.currentRow()
        if current_row >= 0:
            self.on_stage_selected(None, None)
    
    def browse_model_file(self):
        """Browse for model file"""
        filename, _ = QFileDialog.getOpenFileName(
            self, "Select Model File", "", 
            "Model files (*.nef *.onnx *.tflite);;All files (*.*)"
        )
        if filename:
            self.model_path_input.setText(filename)
            self.update_current_stage()
    
    def update_performance_estimates(self, config):
        """Calculate and display performance estimates"""
        dongles = config['dongles']
        
        # Simple performance estimation based on dongle count
        base_fps = 30  # Base FPS per dongle
        estimated_fps = dongles * base_fps
        latency = 1000 / max(estimated_fps, 1)  # ms
        throughput = estimated_fps * 1  # frames per second
        
        self.estimated_fps_label.setText(f"{estimated_fps:.1f} FPS")
        self.estimated_latency_label.setText(f"{latency:.1f} ms")
        self.throughput_label.setText(f"{throughput:.1f} frames/sec")
    
    def update_resource_summary(self):
        """Update resource usage summary"""
        total_dongles = sum(config['dongles'] for config in self.stage_configs)
        self.total_dongles_label.setText(f"{total_dongles} / {self.total_dongles_available}")
        
        # Calculate overall pipeline FPS (limited by slowest stage)
        if self.stage_configs:
            min_fps = min(config['dongles'] * 30 for config in self.stage_configs)
            self.pipeline_fps_label.setText(f"{min_fps:.1f} FPS")
        else:
            self.pipeline_fps_label.setText("--")
    
    def show_performance_panel(self):
        """Show performance estimation panel"""
        if not self.stage_configs:
            QMessageBox.warning(self, "No Stages", "Please add at least one stage before proceeding.")
            return
        
        # Validate stage configurations
        for config in self.stage_configs:
            if not config['name'].strip():
                QMessageBox.warning(self, "Invalid Configuration", "All stages must have names.")
                return
        
        self.accept()
        
        # Show performance panel
        perf_panel = PerformanceEstimationPanel(self.stage_configs, self.parent())
        perf_panel.exec_()
    
    def get_stage_configs(self):
        """Get configured stages"""
        return self.stage_configs


# Simplified versions of the other dialogs for now
class PerformanceEstimationPanel(QDialog):
    """Performance estimation and tweaking panel"""
    
    def __init__(self, stage_configs, parent=None):
        super().__init__(parent)
        self.stage_configs = stage_configs
        self.setWindowTitle("Performance Estimation & Optimization")
        self.setMinimumSize(600, 400)
        self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET)
        self.setup_ui()
    
    def setup_ui(self):
        layout = QVBoxLayout(self)
        
        # Header
        header = QLabel("Pipeline Performance Analysis")
        header.setFont(QFont("Arial", 16, QFont.Bold))
        header.setStyleSheet("color: #89b4fa; margin-bottom: 10px;")
        layout.addWidget(header)
        
        # Performance summary
        summary_text = f"Analyzing {len(self.stage_configs)} stages:\n"
        total_dongles = sum(config['dongles'] for config in self.stage_configs)
        min_fps = min(config['dongles'] * 30 for config in self.stage_configs) if self.stage_configs else 0
        
        summary_text += f"• Total dongles: {total_dongles}\n"
        summary_text += f"• Pipeline FPS: {min_fps:.1f}\n"
        summary_text += f"• Estimated latency: {sum(1000/(config['dongles']*30) for config in self.stage_configs):.1f} ms"
        
        summary_label = QLabel(summary_text)
        summary_label.setStyleSheet("padding: 20px; background-color: #313244; border-radius: 8px;")
        layout.addWidget(summary_label)
        
        # Buttons
        button_layout = QHBoxLayout()
        self.deploy_btn = QPushButton("Next: Save & Deploy")
        self.deploy_btn.clicked.connect(self.show_deploy_dialog)
        self.back_btn = QPushButton("← Back")
        self.back_btn.clicked.connect(self.reject)
        
        button_layout.addWidget(self.back_btn)
        button_layout.addStretch()
        button_layout.addWidget(self.deploy_btn)
        layout.addLayout(button_layout)
    
    def show_deploy_dialog(self):
        """Show deployment dialog"""
        self.accept()
        deploy_dialog = SaveDeployDialog(self.stage_configs, self.parent())
        deploy_dialog.exec_()


class SaveDeployDialog(QDialog):
    """Save and Deploy dialog with cost estimation"""
    
    def __init__(self, stage_configs, parent=None):
        super().__init__(parent)
        self.stage_configs = stage_configs
        self.setWindowTitle("Save & Deploy Pipeline")
        self.setMinimumSize(600, 400)
        self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET)
        self.setup_ui()
    
    def setup_ui(self):
        layout = QVBoxLayout(self)
        
        # Header
        header = QLabel("Save & Deploy Configuration")
        header.setFont(QFont("Arial", 16, QFont.Bold))
        header.setStyleSheet("color: #89b4fa; margin-bottom: 10px;")
        layout.addWidget(header)
        
        # Export options
        export_group = QGroupBox("Export Configuration")
        export_layout = QFormLayout(export_group)
        
        self.config_name_input = QLineEdit()
        self.config_name_input.setText(f"pipeline_config_{time.strftime('%Y%m%d_%H%M%S')}")
        export_layout.addRow("Configuration Name:", self.config_name_input)
        
        self.export_format_combo = QComboBox()
        self.export_format_combo.addItems(["Python Script", "JSON Config", "YAML Pipeline"])
        export_layout.addRow("Export Format:", self.export_format_combo)
        
        self.export_btn = QPushButton("💾 Export Configuration")
        self.export_btn.clicked.connect(self.export_configuration)
        export_layout.addRow("", self.export_btn)
        
        layout.addWidget(export_group)
        
        # Cost estimation
        total_dongles = sum(config['dongles'] for config in self.stage_configs)
        cost_text = f"Cost Estimation:\n"
        cost_text += f"• Hardware: {total_dongles} dongles (~${total_dongles * 299:,})\n"
        cost_text += f"• Power: {total_dongles * 5}W continuous\n"
        cost_text += f"• Monthly operating: ~${total_dongles * 15:.2f}"
        
        cost_label = QLabel(cost_text)
        cost_label.setStyleSheet("padding: 20px; background-color: #313244; border-radius: 8px;")
        layout.addWidget(cost_label)
        
        # Buttons
        button_layout = QHBoxLayout()
        self.deploy_btn = QPushButton("🚀 Deploy Pipeline")
        self.deploy_btn.clicked.connect(self.deploy_pipeline)
        self.back_btn = QPushButton("← Back")
        self.back_btn.clicked.connect(self.reject)
        
        button_layout.addWidget(self.back_btn)
        button_layout.addStretch()
        button_layout.addWidget(self.deploy_btn)
        layout.addLayout(button_layout)
    
    def export_configuration(self):
        """Export configuration to file"""
        format_type = self.export_format_combo.currentText()
        config_name = self.config_name_input.text().strip()
        
        if not config_name:
            QMessageBox.warning(self, "Invalid Name", "Please enter a configuration name.")
            return
        
        # Generate content based on format
        if format_type == "Python Script":
            content = self.generate_python_script()
            ext = ".py"
        elif format_type == "JSON Config":
            content = json.dumps({
                "stages": [{"name": c['name'], "dongles": c['dongles'], "port_ids": c['port_ids']} 
                          for c in self.stage_configs]
            }, indent=2)
            ext = ".json"
        else:  # YAML
            content = "stages:\n"
            for config in self.stage_configs:
                content += f"  - name: {config['name']}\n    dongles: {config['dongles']}\n    port_ids: {config['port_ids']}\n"
            ext = ".yaml"
        
        # Save file
        filename, _ = QFileDialog.getSaveFileName(
            self, f"Save {format_type}", f"{config_name}{ext}",
            f"{format_type} files (*{ext});;All files (*.*)"
        )
        
        if filename:
            try:
                with open(filename, 'w') as f:
                    f.write(content)
                QMessageBox.information(self, "Exported", f"Configuration exported to {os.path.basename(filename)}")
            except Exception as e:
                QMessageBox.critical(self, "Export Error", f"Failed to export: {str(e)}")
    
    def generate_python_script(self):
        """Generate Python script"""
        script = '''#!/usr/bin/env python3
"""
Generated Pipeline Configuration
"""

from src.cluster4npu.InferencePipeline import InferencePipeline, StageConfig

def main():
    stage_configs = [
'''
        
        for config in self.stage_configs:
            port_ids = config['port_ids'].split(',') if ',' in config['port_ids'] else [28, 30]
            script += f'''        StageConfig(
            stage_id="{config['name'].lower().replace(' ', '_')}",
            port_ids={port_ids},
            scpu_fw_path="fw_scpu.bin",
            ncpu_fw_path="fw_ncpu.bin",
            model_path="{config.get('model_path', 'model.nef')}",
            upload_fw=True
        ),
'''
        
        script += '''    ]
    
    pipeline = InferencePipeline(stage_configs)
    pipeline.initialize()
    pipeline.start()
    
    try:
        import time
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        pipeline.stop()

if __name__ == "__main__":
    main()
'''
        return script
    
    def deploy_pipeline(self):
        """Deploy the pipeline"""
        QMessageBox.information(
            self, "Deployment Started",
            "Pipeline deployment initiated!\n\nThis would start the actual hardware deployment process."
        )
        self.accept()


if __name__ == '__main__':
    app.setFont(QFont("Arial", 9))
    dashboard = DashboardLogin()
    dashboard.show()
    sys.exit(app.exec_())