"""
System Monitor Widget for PyQt6

Real-time system monitoring with CPU, RAM, GPU, and VRAM usage graphs.
Migrated from Textual TUI with full feature parity.
"""

import logging
import time
from typing import Optional
from datetime import datetime

from PyQt6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame, QGridLayout, QSizePolicy, QPushButton, QSplitter
)
from PyQt6.QtCore import QTimer, pyqtSignal, QMetaObject, Qt
from PyQt6.QtGui import QFont, QResizeEvent

# Lazy import of PyQtGraph to avoid blocking startup
# PyQtGraph imports cupy at module level, which takes 8+ seconds
# We'll import it on-demand when creating graphs
PYQTGRAPH_AVAILABLE = None  # None = not checked, True = available, False = not available
pg = None  # Will be set when pyqtgraph is imported

# Import the SystemMonitorCore service (framework-agnostic)
from pyqt_reactive.theming import StyleSheetGenerator
from pyqt_reactive.theming import ColorScheme

from pyqt_reactive.services.system_monitor_core import SystemMonitorCore
from pyqt_reactive.services.persistent_system_monitor import PersistentSystemMonitor
from pyqt_reactive.protocols import get_form_config

logger = logging.getLogger(__name__)


class SystemMonitorWidget(QWidget):
    """
    PyQt6 System Monitor Widget.
    
    Displays real-time system metrics with graphs for CPU, RAM, GPU, and VRAM usage.
    Provides the same functionality as the Textual SystemMonitorTextual widget.
    """
    
    # Declarative button configuration (matches AbstractManagerWidget pattern)
    BUTTON_CONFIGS = [
        ("Global Config", "global_config", "Open global configuration editor"),
        ("Log Viewer", "log_viewer", "Open log viewer window"),
        ("Custom Functions", "custom_functions", "Manage custom functions"),
        ("Test Plate", "test_plate", "Generate synthetic test plate"),
    ]
    BUTTON_GRID_COLUMNS = 0  # Single row (all buttons next to each other)
    
    # Signals
    metrics_updated = pyqtSignal(dict)  # Emitted when metrics are updated
    _pyqtgraph_loaded = pyqtSignal()  # Internal signal for async pyqtgraph loading
    _pyqtgraph_failed = pyqtSignal()  # Internal signal for async pyqtgraph loading failure
    
    # Button action signals
    show_global_config = pyqtSignal()  # Request to show global config
    show_log_viewer = pyqtSignal()  # Request to show log viewer
    show_custom_functions = pyqtSignal()  # Request to show custom functions manager
    show_test_plate_generator = pyqtSignal()  # Request to show synthetic plate generator
    
    def __init__(self,
                 color_scheme: Optional[ColorScheme] = None,
                 config: Optional[object] = None,
                 parent=None):
        """
        Initialize the system monitor widget.

        Args:
            color_scheme: Color scheme for styling (optional, uses default if None)
            config: GUI configuration (optional, uses default if None)
            parent: Parent widget
        """
        super().__init__(parent)

        # Initialize configuration
        self.config = config or get_form_config()
        self.monitor_config = self._build_monitor_config(self.config)

        # Initialize color scheme and style generator
        self.color_scheme = color_scheme or ColorScheme()
        self.style_generator = StyleSheetGenerator(self.color_scheme)

        # Calculate monitoring parameters from configuration
        update_interval = self.monitor_config.update_interval_seconds
        history_length = self.monitor_config.calculated_max_data_points

        # Core monitoring - use persistent thread for non-blocking metrics collection
        self.monitor = SystemMonitorCore(history_length=history_length)  # Match the dynamic history length

        self.persistent_monitor = PersistentSystemMonitor(
            update_interval=update_interval,
            history_length=history_length
        )
        # No timer needed - the persistent thread handles timing

        # Track graph layout mode (True = side-by-side, False = stacked)
        # MUST be set before setup_ui() since create_pyqtgraph_section() uses it
        self._graphs_side_by_side = True

        # Delay monitoring start until widget is shown (fixes WSL2 hanging)
        self._monitoring_started = False

        # Render-timer interpolation state
        self._render_timer = QTimer(self)
        self._render_timer.setTimerType(Qt.TimerType.PreciseTimer)
        self._render_timer.timeout.connect(self._render_frame)
        self._last_metrics_time = None
        self._last_metrics = None
        self._next_metrics = None

        # Setup UI
        self.setup_ui()
        self.setup_connections()

        # Set size policy to minimum - let surrounding widgets expand into this space
        self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
        # Set minimum size for usability - more vertically compact
        self.setMinimumSize(200, 160)

        logger.debug("System monitor widget initialized")

    def _build_monitor_config(self, config):
        """Build a safe monitor config with defaults for missing attributes."""
        from types import SimpleNamespace

        default_colors = {
            "cpu": "cyan",
            "ram": "lime",
            "gpu": "orange",
            "vram": "magenta",
        }
        monitor = getattr(config, "performance_monitor", None)
        if monitor is None:
            update_fps = 5.0
            render_fps = 60.0
            history_duration_seconds = 60.0
            update_interval_seconds = 1.0 / update_fps
            calculated_max_data_points = int(history_duration_seconds / update_interval_seconds)
            return SimpleNamespace(
                update_fps=update_fps,
                render_fps=render_fps,
                history_duration_seconds=history_duration_seconds,
                update_interval_seconds=update_interval_seconds,
                calculated_max_data_points=calculated_max_data_points,
                antialiasing=True,
                show_grid=True,
                line_width=2.0,
                chart_colors=default_colors,
            )

        update_fps = getattr(monitor, "update_fps", 5.0)
        render_fps = getattr(monitor, "render_fps", 60.0)
        history_duration_seconds = getattr(monitor, "history_duration_seconds", 60.0)
        update_interval_seconds = getattr(
            monitor,
            "update_interval_seconds",
            1.0 / update_fps if update_fps else 1.0,
        )
        calculated_max_data_points = getattr(
            monitor,
            "calculated_max_data_points",
            int(history_duration_seconds / update_interval_seconds) if update_interval_seconds else 60,
        )
        return SimpleNamespace(
            update_fps=update_fps,
            render_fps=render_fps,
            history_duration_seconds=history_duration_seconds,
            update_interval_seconds=update_interval_seconds,
            calculated_max_data_points=calculated_max_data_points,
            antialiasing=getattr(monitor, "antialiasing", True),
            show_grid=getattr(monitor, "show_grid", True),
            line_width=getattr(monitor, "line_width", 2.0),
            chart_colors=getattr(monitor, "chart_colors", default_colors),
        )

    def create_loading_placeholder(self) -> QWidget:
        """
        Create a simple loading placeholder shown while PyQtGraph loads.

        Returns:
            Simple loading label widget
        """
        from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget
        from PyQt6.QtCore import Qt

        placeholder = QWidget()
        layout = QVBoxLayout(placeholder)

        label = QLabel("Loading system monitor...")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(label)

        return placeholder

    def _load_pyqtgraph_async(self):
        """
        Load PyQtGraph asynchronously using QTimer to avoid blocking.

        We use QTimer instead of threading because Python's GIL causes background
        thread imports to block the main thread anyway. By using QTimer with a delay,
        we give the user time to interact with the UI before the import happens.
        """
        # Load immediately - no artificial delay
        QTimer.singleShot(0, self._import_pyqtgraph_main_thread)
        logger.info("PyQtGraph loading...")

    def _import_pyqtgraph_main_thread(self):
        """Import PyQtGraph in main thread after delay."""
        global PYQTGRAPH_AVAILABLE, pg

        try:
            logger.info("⏳ Loading PyQtGraph (UI will freeze for ~8 seconds)...")
            logger.info("📦 Importing pyqtgraph module...")
            import pyqtgraph as pg_module
            logger.info("📦 PyQtGraph module imported")

            logger.info("🔧 Initializing PyQtGraph (loading GPU libraries: cupy, numpy, etc.)...")
            pg = pg_module
            PYQTGRAPH_AVAILABLE = True
            logger.info("✅ PyQtGraph loaded successfully (GPU libraries ready)")

            # Flush logs so startup screen can read them
            import logging as _logging
            for _h in _logging.getLogger().handlers:
                try:
                    _h.flush()
                except Exception:
                    pass

            # Schedule UI switch on next event loop tick so startup screen can update
            from PyQt6.QtCore import QTimer as _QTimer
            _QTimer.singleShot(0, self._switch_to_pyqtgraph_ui)
        except ImportError as e:
            logger.warning(f"❌ PyQtGraph not available: {e}")
            PYQTGRAPH_AVAILABLE = False

            # Schedule fallback switch similarly
            from PyQt6.QtCore import QTimer as _QTimer
            _QTimer.singleShot(0, self._switch_to_fallback_ui)

    def _switch_to_pyqtgraph_ui(self):
        """Switch from loading placeholder to PyQtGraph UI (called in main thread)."""
        # Remove loading placeholder from graphs container
        old_widget = self.monitoring_widget
        self.graphs_layout.removeWidget(old_widget)
        old_widget.deleteLater()

        # Create PyQtGraph section
        self.monitoring_widget = self.create_pyqtgraph_section()
        # Add to graphs container layout
        self.graphs_layout.addWidget(self.monitoring_widget)

        logger.info("Switched to PyQtGraph UI")

    def _switch_to_fallback_ui(self):
        """Switch from loading placeholder to fallback UI (called in main thread)."""
        # Remove loading placeholder from graphs container
        old_widget = self.monitoring_widget
        self.graphs_layout.removeWidget(old_widget)
        old_widget.deleteLater()

        # Create fallback section
        self.monitoring_widget = self.create_fallback_section()
        # Add to graphs container layout
        self.graphs_layout.addWidget(self.monitoring_widget)

        logger.info("Switched to fallback UI (PyQtGraph not available)")

    def showEvent(self, event):
        """Handle widget show event - start monitoring when widget becomes visible."""
        super().showEvent(event)
        if not self._monitoring_started:
            # Start monitoring only when widget is actually shown
            # This prevents WSL2 hanging issues during initialization
            self.start_monitoring()
            self._start_render_timer()
            self._monitoring_started = True
            logger.debug("System monitoring started on widget show")

    def resizeEvent(self, event: QResizeEvent):
        """Handle widget resize - adjust font sizes dynamically."""
        super().resizeEvent(event)
        # Defer font size update until after layout is complete
        if hasattr(self, 'info_widget'):
            # Use a timer to update after the layout has settled
            from PyQt6.QtCore import QTimer
            QTimer.singleShot(0, self._update_font_sizes_from_panel)

    def closeEvent(self, event):
        """Handle widget close event - cleanup resources."""
        self.cleanup()
        super().closeEvent(event)

    def __del__(self):
        """Destructor - ensure cleanup happens."""
        try:
            self.cleanup()
        except:
            pass  # Ignore errors during destruction

    def _update_font_sizes_from_panel(self):
        """Update font sizes based on the actual info panel width."""
        if not hasattr(self, 'info_widget'):
            return

        # Use the actual info panel width, not the whole widget width
        panel_width = self.info_widget.width()

        # Larger font sizes for better readability
        # Label font: 10-13pt based on panel width
        label_size = max(10, min(13, panel_width // 50))

        # Update all label fonts
        if hasattr(self, 'cpu_cores_label'):
            for label_pair in [
                self.cpu_cores_label, self.cpu_freq_label,
                self.ram_total_label, self.ram_used_label,
                self.gpu_name_label, self.gpu_temp_label, self.vram_label
            ]:
                # Update key label
                key_font = QFont("Arial", label_size)
                label_pair[0].setFont(key_font)

                # Update value label (bold)
                value_font = QFont("Arial", label_size)
                value_font.setBold(True)
                label_pair[1].setFont(value_font)
    
    def setup_ui(self):
        """Setup the user interface with proper splitter hierarchy."""
        main_layout = QVBoxLayout(self)
        main_layout.setContentsMargins(2, 2, 2, 2)
        main_layout.setSpacing(2)

        # Header (title + status) - similar to AbstractManagerWidget
        header = self._create_header()
        main_layout.addWidget(header)

        # MAIN HSPLIT: Left side (with nested VSPLIT) | Right side (graphs)
        main_splitter = QSplitter(Qt.Orientation.Horizontal)

        # LEFT SIDE: VSPLIT with System Info (top) and Buttons (bottom)
        left_side = self._create_left_side_with_vsplit()
        main_splitter.addWidget(left_side)

        # RIGHT SIDE: Performance Monitor (graphs)
        self.graphs_container = QWidget()
        self.graphs_container.setMinimumSize(1, 1)  # Allow to shrink to minimum
        self.graphs_layout = QVBoxLayout(self.graphs_container)
        self.graphs_layout.setContentsMargins(0, 0, 0, 0)
        self.graphs_layout.setSpacing(0)

        # Start with loading placeholder
        self.monitoring_widget = self.create_loading_placeholder()
        self.graphs_layout.addWidget(self.monitoring_widget)

        main_splitter.addWidget(self.graphs_container)

        # Set sizes: smaller left side, larger graphs area
        main_splitter.setSizes([80, 240])
        main_splitter.setStretchFactor(0, 0)  # Don't expand left side
        main_splitter.setStretchFactor(1, 1)  # Graphs can expand horizontally
        # Cap maximum height for graphs to keep vertical compactness
        self.graphs_container.setMaximumHeight(180)

        main_layout.addWidget(main_splitter)

        # Apply centralized styling
        self.setStyleSheet(self.style_generator.generate_system_monitor_style())

        # Load PyQtGraph asynchronously
        self._load_pyqtgraph_async()

    def _create_header(self) -> QWidget:
        """Create header with title and status label (similar to AbstractManagerWidget)."""
        header = QWidget()
        header.setMinimumHeight(30)  # Ensure title and status are visible
        header_layout = QHBoxLayout(header)
        header_layout.setContentsMargins(5, 5, 5, 5)

        # Title label
        title_label = QLabel("System Monitor")
        title_label.setFont(QFont("Arial", 12, QFont.Weight.Bold))
        title_label.setStyleSheet(
            f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)};"
        )
        header_layout.addWidget(title_label)

        header_layout.addStretch()

        # Status label
        self.status_label = QLabel("Ready")
        self.status_label.setStyleSheet(
            f"color: {self.color_scheme.to_hex(self.color_scheme.status_success)}; "
            f"font-weight: bold;"
        )
        header_layout.addWidget(self.status_label)

        return header

    def _create_left_side_with_vsplit(self) -> QWidget:
        """Create left side with VSPLIT: System Info on top, buttons on bottom."""
        splitter = QSplitter(Qt.Orientation.Vertical)

        # Top: System info panel
        self.info_widget = self.create_info_panel()
        splitter.addWidget(self.info_widget)

        # Bottom: Button panel - store reference for API access
        self.button_panel = self._create_button_panel()
        splitter.addWidget(self.button_panel)

        # Set sizes: fixed sizes - don't expand
        splitter.setSizes([200, 80])
        splitter.setStretchFactor(0, 0)  # Info doesn't expand
        splitter.setStretchFactor(1, 0)  # Buttons don't expand

        return splitter

    def _create_button_panel(self) -> QWidget:
        """Create button panel using reusable ButtonPanel component."""
        from pyqt_reactive.widgets.shared.button_panel import ButtonPanel
        from PyQt6.QtWidgets import QSizePolicy
        
        panel = ButtonPanel(
            button_configs=self.BUTTON_CONFIGS,
            on_action=self.handle_button_action,
            style_generator=self.style_generator,
            grid_columns=self.BUTTON_GRID_COLUMNS,
            parent=self
        )
        # Prevent panel from expanding vertically
        panel.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
        return panel
    
    def handle_button_action(self, action_id: str):
        """Handle button actions declaratively."""
        if action_id == "global_config":
            self.show_global_config.emit()
        elif action_id == "log_viewer":
            self.show_log_viewer.emit()
        elif action_id == "custom_functions":
            self.show_custom_functions.emit()
        elif action_id == "test_plate":
            self.show_test_plate_generator.emit()
    
    def _force_refresh(self):
        """Force an immediate refresh of system metrics."""
        logger.debug("Force refresh requested")
        # The persistent monitor updates automatically, but we could add
        # immediate refresh logic here if needed
    
    def create_ascii_widget(self) -> QWidget:
        """Create the ASCII art widget for the bottom."""
        widget = QWidget()
        layout = QHBoxLayout(widget)
        layout.setContentsMargins(0, 0, 0, 0)
        
        # ASCII header
        self.header_label = QLabel(self.get_ascii_header())
        self.header_label.setObjectName("header_label")
        font = QFont("Courier", 10)
        font.setBold(True)
        self.header_label.setFont(font)
        self.header_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(self.header_label)
        
        return widget

    def create_info_panel(self) -> QWidget:
        """Create a system information panel matching AbstractManagerWidget styling."""
        panel = QWidget()
        panel.setObjectName("info_panel")
        panel.setMinimumSize(1, 1)  # Allow to shrink to minimum

        layout = QVBoxLayout(panel)
        layout.setContentsMargins(8, 8, 8, 8)
        layout.setSpacing(6)

        # Two-column grid layout with tighter spacing
        # Grid has 5 columns: [Label1, Value1, Spacer, Label2, Value2]
        info_grid = QGridLayout()
        info_grid.setHorizontalSpacing(8)
        info_grid.setVerticalSpacing(10)
        info_grid.setColumnStretch(1, 0)  # Left value column - no stretch
        info_grid.setColumnMinimumWidth(2, 15)  # Minimal spacer between columns
        info_grid.setColumnStretch(4, 0)  # Right value column - no stretch

        # Left column - CPU and RAM info (shorter labels)
        self.cpu_cores_label = self.create_info_row("Cores:", "—")
        self.cpu_freq_label = self.create_info_row("Freq:", "—")
        self.ram_total_label = self.create_info_row("RAM:", "—")
        self.ram_used_label = self.create_info_row("Used:", "—")

        info_grid.addWidget(self.cpu_cores_label[0], 0, 0)
        info_grid.addWidget(self.cpu_cores_label[1], 0, 1)
        info_grid.addWidget(self.cpu_freq_label[0], 1, 0)
        info_grid.addWidget(self.cpu_freq_label[1], 1, 1)
        info_grid.addWidget(self.ram_total_label[0], 2, 0)
        info_grid.addWidget(self.ram_total_label[1], 2, 1)
        info_grid.addWidget(self.ram_used_label[0], 3, 0)
        info_grid.addWidget(self.ram_used_label[1], 3, 1)

        # Right column - GPU info (will be hidden if no GPU)
        self.gpu_name_label = self.create_info_row("GPU:", "—")
        self.gpu_temp_label = self.create_info_row("Temp:", "—")
        self.vram_label = self.create_info_row("VRAM:", "—")

        info_grid.addWidget(self.gpu_name_label[0], 0, 3)
        info_grid.addWidget(self.gpu_name_label[1], 0, 4)
        info_grid.addWidget(self.gpu_temp_label[0], 1, 3)
        info_grid.addWidget(self.gpu_temp_label[1], 1, 4)
        info_grid.addWidget(self.vram_label[0], 2, 3)
        info_grid.addWidget(self.vram_label[1], 2, 4)

        layout.addLayout(info_grid)
        # No stretch - panel takes only needed space

        # Schedule initial font size update after panel is shown
        QTimer.singleShot(100, self._update_font_sizes_from_panel)

        return panel

    def create_info_row(self, label_text: str, value_text: str) -> tuple:
        """Create a label-value pair for the info panel (font size set dynamically in resizeEvent)."""
        label = QLabel(label_text)
        label.setObjectName("info_label_key")

        value = QLabel(value_text)
        value.setObjectName("info_label_value")

        return (label, value)
    
    def create_pyqtgraph_section(self) -> QWidget:
        """
        Create PyQtGraph-based monitoring section with consolidated graphs.

        Returns:
            Widget containing consolidated PyQtGraph plots
        """
        widget = QWidget()
        main_layout = QVBoxLayout(widget)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.setSpacing(5)

        # Container for graphs that we can re-layout
        self.graph_container = QWidget()
        self.graph_layout = QGridLayout(self.graph_container)
        self.graph_layout.setSpacing(2)  # Minimal spacing between graphs
        self.graph_layout.setContentsMargins(0, 0, 0, 0)  # No margins

        # Configure PyQtGraph based on config settings
        pg.setConfigOption('background', self.color_scheme.to_hex(self.color_scheme.window_bg))
        pg.setConfigOption('foreground', 'white')
        pg.setConfigOption('antialias', self.monitor_config.antialiasing)

        # Create consolidated PyQtGraph plots with minimal size constraints
        self.cpu_gpu_plot = pg.PlotWidget(title="CPU/GPU Usage")
        self.ram_vram_plot = pg.PlotWidget(title="RAM/VRAM Usage")
        
        # Allow plots to shrink to very small size
        self.cpu_gpu_plot.setMinimumSize(1, 1)
        self.ram_vram_plot.setMinimumSize(1, 1)

        # Disable mouse interaction on plots
        self.cpu_gpu_plot.setMouseEnabled(x=False, y=False)
        self.ram_vram_plot.setMouseEnabled(x=False, y=False)
        self.cpu_gpu_plot.setMenuEnabled(False)
        self.ram_vram_plot.setMenuEnabled(False)

        # Store plot data items for efficient updates using configured colors and line width
        colors = self.monitor_config.chart_colors
        line_width = self.monitor_config.line_width

        # CPU/GPU plot curves
        self.cpu_curve = self.cpu_gpu_plot.plot(pen=pg.mkPen(colors['cpu'], width=line_width), name='CPU')
        self.gpu_curve = self.cpu_gpu_plot.plot(pen=pg.mkPen(colors['gpu'], width=line_width), name='GPU')

        # RAM/VRAM plot curves
        self.ram_curve = self.ram_vram_plot.plot(pen=pg.mkPen(colors['ram'], width=line_width), name='RAM')
        self.vram_curve = self.ram_vram_plot.plot(pen=pg.mkPen(colors['vram'], width=line_width), name='VRAM')

        # Style CPU/GPU plot - minimal padding
        self.cpu_gpu_plot.setBackground(self.color_scheme.to_hex(self.color_scheme.panel_bg))
        self.cpu_gpu_plot.setYRange(0, 100)
        # X axis shows "seconds ago", so range is (-history, 0) with 0 = now (right edge)
        self.cpu_gpu_plot.setXRange(-self.monitor_config.history_duration_seconds, 0)
        self.cpu_gpu_plot.showGrid(x=self.monitor_config.show_grid, y=self.monitor_config.show_grid, alpha=0.3)
        # Disable auto-ranging so manual panning works reliably
        _cpu_vb = self.cpu_gpu_plot.getPlotItem().getViewBox()
        _cpu_vb.setAutoPan(x=False, y=False)
        _cpu_vb.disableAutoRange()

        # Minimize left axis
        self.cpu_gpu_plot.getAxis('left').setTextPen('white')
        self.cpu_gpu_plot.getAxis('left').setStyle(tickLength=-5)
        self.cpu_gpu_plot.getAxis('left').setWidth(35)  # Minimal width for y-axis

        # Hide bottom axis completely
        self.cpu_gpu_plot.getAxis('bottom').setHeight(0)
        self.cpu_gpu_plot.getAxis('bottom').setStyle(showValues=False)

        # Minimize all margins and padding
        self.cpu_gpu_plot.getPlotItem().setContentsMargins(0, 0, 0, 0)
        self.cpu_gpu_plot.getViewBox().setDefaultPadding(0)

        # Style RAM/VRAM plot - minimal padding
        self.ram_vram_plot.setBackground(self.color_scheme.to_hex(self.color_scheme.panel_bg))
        self.ram_vram_plot.setYRange(0, 100)
        # X axis shows "seconds ago", so range is (-history, 0) with 0 = now (right edge)
        self.ram_vram_plot.setXRange(-self.monitor_config.history_duration_seconds, 0)
        self.ram_vram_plot.showGrid(x=self.monitor_config.show_grid, y=self.monitor_config.show_grid, alpha=0.3)
        # Disable auto-ranging so manual panning works reliably
        _ram_vb = self.ram_vram_plot.getPlotItem().getViewBox()
        _ram_vb.setAutoPan(x=False, y=False)
        _ram_vb.disableAutoRange()

        # Minimize left axis
        self.ram_vram_plot.getAxis('left').setTextPen('white')
        self.ram_vram_plot.getAxis('left').setStyle(tickLength=-5)
        self.ram_vram_plot.getAxis('left').setWidth(35)  # Minimal width for y-axis

        # Hide bottom axis completely
        self.ram_vram_plot.getAxis('bottom').setHeight(0)
        self.ram_vram_plot.getAxis('bottom').setStyle(showValues=False)

        # Minimize all margins and padding
        self.ram_vram_plot.getPlotItem().setContentsMargins(0, 0, 0, 0)
        self.ram_vram_plot.getViewBox().setDefaultPadding(0)

        # Add plots to grid layout (side-by-side by default)
        self._update_graph_layout()

        # No stretch - let container take minimum size
        main_layout.addWidget(self.graph_container, 0)

        return widget
    
    def create_layout_toggle_button(self) -> QPushButton:
        """
        Create a toggle button for switching graph layouts.
        This button is meant to be added to the main window's status bar.

        Returns:
            QPushButton configured for layout toggling
        """
        self.layout_toggle_button = QPushButton("⬍ Stack")
        self.layout_toggle_button.setMaximumWidth(80)
        self.layout_toggle_button.setMaximumHeight(24)
        self.layout_toggle_button.setToolTip("Toggle between side-by-side and stacked layout")
        self.layout_toggle_button.clicked.connect(self.toggle_graph_layout)

        # Style the button to match parameter form manager style
        button_styles = self.style_generator.generate_config_button_styles()
        self.layout_toggle_button.setStyleSheet(button_styles["reset"])

        return self.layout_toggle_button

    def toggle_graph_layout(self):
        """Toggle between side-by-side and stacked graph layouts."""
        self._graphs_side_by_side = not self._graphs_side_by_side
        self._update_graph_layout()

        # Update button text via ButtonPanel API
        if hasattr(self, 'button_panel'):
            if self._graphs_side_by_side:
                self.button_panel.set_button_text("toggle_layout", "Stack")
            else:
                self.button_panel.set_button_text("toggle_layout", "Side-by-Side")

    def _update_graph_layout(self):
        """Update the graph layout based on current mode."""
        # Remove all widgets from layout
        while self.graph_layout.count():
            item = self.graph_layout.takeAt(0)
            if item.widget():
                item.widget().setParent(None)

        if self._graphs_side_by_side:
            # Side-by-side: 1 row, 2 columns
            self.graph_layout.addWidget(self.cpu_gpu_plot, 0, 0)
            self.graph_layout.addWidget(self.ram_vram_plot, 0, 1)
        else:
            # Stacked: 2 rows, 1 column
            self.graph_layout.addWidget(self.cpu_gpu_plot, 0, 0)
            self.graph_layout.addWidget(self.ram_vram_plot, 1, 0)

    def create_fallback_section(self) -> QWidget:
        """
        Create fallback text-based monitoring section.
        
        Returns:
            Widget containing text-based display
        """
        widget = QFrame()
        widget.setFrameStyle(QFrame.Shape.Box)
        widget.setStyleSheet(f"""
            QFrame {{
                background-color: {self.color_scheme.to_hex(self.color_scheme.panel_bg)};
                border: 1px solid {self.color_scheme.to_hex(self.color_scheme.border_color)};
                border-radius: 3px;
                padding: 10px;
            }}
        """)
        
        layout = QVBoxLayout(widget)
        
        self.fallback_label = QLabel("")
        self.fallback_label.setFont(QFont("Courier", 10))
        self.fallback_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)};")
        layout.addWidget(self.fallback_label)
        
        return widget
    
    def setup_connections(self):
        """Setup signal/slot connections."""
        self.metrics_updated.connect(self.update_display)

        # Connect persistent monitor signals
        self.persistent_monitor.connect_signals(
            metrics_callback=self.on_metrics_updated,
            error_callback=self.on_metrics_error
        )
    
    def start_monitoring(self):
        """Start the persistent monitoring thread."""
        self.persistent_monitor.start_monitoring()
        logger.debug("System monitoring started")

    def stop_monitoring(self):
        """Stop the persistent monitoring thread."""
        self.persistent_monitor.stop_monitoring()
        self._stop_render_timer()
        logger.debug("System monitoring stopped")

    def cleanup(self):
        """Clean up widget resources."""
        try:
            logger.debug("Cleaning up SystemMonitorWidget...")

            # Stop monitoring first
            self.stop_monitoring()

            # Clean up pyqtgraph plots
            if PYQTGRAPH_AVAILABLE and hasattr(self, 'cpu_plot'):
                try:
                    self.cpu_plot.clear()
                    self.ram_plot.clear()
                    self.gpu_plot.clear()
                    self.vram_plot.clear()

                    # Clear plot widgets
                    if hasattr(self, 'cpu_plot_widget'):
                        self.cpu_plot_widget.close()
                    if hasattr(self, 'ram_plot_widget'):
                        self.ram_plot_widget.close()
                    if hasattr(self, 'gpu_plot_widget'):
                        self.gpu_plot_widget.close()
                    if hasattr(self, 'vram_plot_widget'):
                        self.vram_plot_widget.close()

                except Exception as e:
                    logger.warning(f"Error cleaning up pyqtgraph plots: {e}")

            # Clear data
            if hasattr(self, 'monitor'):
                self.monitor.cpu_history.clear()
                self.monitor.ram_history.clear()
                self.monitor.gpu_history.clear()
                self.monitor.vram_history.clear()
                self.monitor.time_stamps.clear()

            logger.debug("SystemMonitorWidget cleanup completed")

        except Exception as e:
            logger.warning(f"Error during SystemMonitorWidget cleanup: {e}")
    
    def on_metrics_updated(self, metrics: dict):
        """Handle metrics update from persistent monitor thread."""
        try:
            # Update the sync monitor's history for compatibility with existing plotting code
            if metrics:
                self.monitor.cpu_history.append(metrics.get('cpu_percent', 0))
                self.monitor.ram_history.append(metrics.get('ram_percent', 0))
                self.monitor.gpu_history.append(metrics.get('gpu_percent', 0))
                self.monitor.vram_history.append(metrics.get('vram_percent', 0))
                self.monitor.time_stamps.append(time.time())

                # Update cached metrics
                self.monitor._current_metrics = metrics.copy()

            # Use QTimer.singleShot to ensure UI update happens on main thread
            from PyQt6.QtCore import QTimer
            QTimer.singleShot(0, lambda: self.metrics_updated.emit(metrics))

        except Exception as e:
            logger.warning(f"Failed to process metrics update: {e}")

    def on_metrics_error(self, error_message: str):
        """Handle metrics collection error."""
        logger.warning(f"Metrics collection failed: {error_message}")
        # Continue with cached/default metrics to keep UI responsive

    def update_display(self, metrics: dict):
        """
        Update the display with new metrics.

        Args:
            metrics: Dictionary of system metrics
        """
        try:
            # Update system info
            self.update_system_info(metrics)

            # Queue metrics for render interpolation
            self._queue_metrics(metrics)

            # Update fallback display immediately if graphs are unavailable
            if PYQTGRAPH_AVAILABLE is False:
                self.update_fallback_display(metrics)
            # else: render timer handles plot updates for smooth interpolation

        except Exception as e:
            logger.warning(f"Failed to update display: {e}")

    def _start_render_timer(self):
        """Start high-FPS render timer for smooth graph updates."""
        render_fps = max(1.0, float(getattr(self.monitor_config, "render_fps", 60.0)))
        interval_ms = max(1, int(1000.0 / render_fps))
        if not self._render_timer.isActive():
            self._render_timer.start(interval_ms)

    def _stop_render_timer(self):
        """Stop render timer."""
        if self._render_timer.isActive():
            self._render_timer.stop()

    def _queue_metrics(self, metrics: dict):
        """Queue metrics for interpolation between data updates."""
        now = time.time()
        if self._next_metrics is None:
            self._last_metrics = metrics
            self._next_metrics = metrics
            self._last_metrics_time = now
            return

        self._last_metrics = self._next_metrics
        self._next_metrics = metrics
        self._last_metrics_time = now

    def _render_frame(self):
        """Render a frame using interpolated metrics and plot data."""
        if PYQTGRAPH_AVAILABLE is not True:
            return

        data_length = len(self.monitor.cpu_history)
        if data_length == 0:
            return

        render_metrics = self._get_interpolated_metrics()
        if render_metrics is None:
            return

        self.update_pyqtgraph_plots(render_metrics)

    def _get_interpolated_metrics(self) -> Optional[dict]:
        """Interpolate between last and next metrics for smooth rendering."""
        if self._last_metrics is None or self._next_metrics is None:
            return None

        update_interval = self.monitor_config.update_interval_seconds
        if update_interval <= 0:
            return self._next_metrics

        now = time.time()
        elapsed = max(0.0, now - (self._last_metrics_time or now))
        t = min(1.0, elapsed / update_interval)

        interpolated = dict(self._next_metrics)
        for key in (
            "cpu_percent",
            "ram_percent",
            "gpu_percent",
            "vram_percent",
        ):
            if key in self._last_metrics and key in self._next_metrics:
                try:
                    start = float(self._last_metrics[key])
                    end = float(self._next_metrics[key])
                    interpolated[key] = start + (end - start) * t
                except (TypeError, ValueError):
                    interpolated[key] = self._next_metrics.get(key)

        return interpolated
    
    def update_pyqtgraph_plots(self, metrics: Optional[dict] = None):
        """Update consolidated PyQtGraph plots with current data - non-blocking and fast."""
        try:
            data_length = len(self.monitor.cpu_history)
            if data_length == 0:
                return

            import numpy as _np

            update_interval = float(self.monitor_config.update_interval_seconds)
            history = float(self.monitor_config.history_duration_seconds)
            now = time.time()

            # Snapshot histories
            cpu_hist = _np.asarray(self.monitor.cpu_history, dtype=_np.float32)
            ram_hist = _np.asarray(self.monitor.ram_history, dtype=_np.float32)
            gpu_hist = _np.asarray(self.monitor.gpu_history, dtype=_np.float32)
            vram_hist = _np.asarray(self.monitor.vram_history, dtype=_np.float32)
            ts = _np.asarray(self.monitor.time_stamps, dtype=_np.float64)

            if update_interval <= 0:
                update_interval = 0.2

            # The timestamp deque is prefilled with zeros. Fill those zeros with
            # synthetic timestamps spaced by update_interval so the plot is
            # visible immediately.
            nz = _np.nonzero(ts > 0)[0]
            if nz.size:
                last_i = int(nz[-1])
                # Fill backwards
                for i in range(last_i - 1, -1, -1):
                    if ts[i] <= 0:
                        ts[i] = ts[i + 1] - update_interval
                # Fill forwards (rare, but keep monotonic)
                for i in range(last_i + 1, data_length):
                    if ts[i] <= 0:
                        ts[i] = ts[i - 1] + update_interval
            else:
                idx = _np.arange(data_length, dtype=_np.float64)
                ts = now - (data_length - 1 - idx) * update_interval

            # Convert to relative x in seconds (negative = older; 0 = now)
            base_x = ts - now

            # Densify tail from newest sample -> now so x scrolls continuously
            try:
                rf = float(getattr(self.monitor_config, "render_fps", 60.0))
                uf = float(getattr(self.monitor_config, "update_fps", 5.0))
                subdivisions = max(4, int(min(64, round(rf / max(1.0, uf)))))
            except Exception:
                subdivisions = 8

            def _series(hist: _np.ndarray, key: str) -> tuple[_np.ndarray, _np.ndarray]:
                if data_length == 1:
                    x0 = float(base_x[-1])
                    y0 = float(hist[-1])
                    y1 = float(metrics.get(key, y0)) if metrics is not None else y0
                    x_tail = _np.linspace(x0, 0.0, subdivisions, dtype=_np.float64)
                    y_tail = _np.linspace(y0, y1, subdivisions, dtype=_np.float32)
                    return x_tail, y_tail

                x0 = float(base_x[-1])
                y0 = float(hist[-1])
                y1 = float(metrics.get(key, y0)) if metrics is not None else y0
                x_tail = _np.linspace(x0, 0.0, subdivisions, dtype=_np.float64)
                y_tail = _np.linspace(y0, y1, subdivisions, dtype=_np.float32)
                x_full = _np.concatenate((base_x[:-1], x_tail))
                y_full = _np.concatenate((hist[:-1], y_tail))
                return x_full, y_full

            x_cpu, y_cpu = _series(cpu_hist, "cpu_percent")
            _, y_gpu = _series(gpu_hist, "gpu_percent")
            _, y_ram = _series(ram_hist, "ram_percent")
            _, y_vram = _series(vram_hist, "vram_percent")

            # Keep x range fixed to the last `history` seconds.
            try:
                self.cpu_gpu_plot.setXRange(-history, 0.0, padding=0)
                self.ram_vram_plot.setXRange(-history, 0.0, padding=0)
            except Exception:
                pass

            self.cpu_curve.setData(x_cpu, y_cpu)
            if _np.any(y_gpu):
                self.gpu_curve.setData(x_cpu, y_gpu)
            else:
                self.gpu_curve.setData([], [])

            # Update CPU/GPU plot title with current values
            if metrics is not None:
                cpu_status = f"{float(metrics.get('cpu_percent', 0.0)):.1f}%"
                if _np.any(y_gpu):
                    gpu_status = f"{float(metrics.get('gpu_percent', 0.0)):.1f}%"
                else:
                    gpu_status = "Not Available"
            else:
                cpu_status = f"{float(y_cpu[-1]) if y_cpu.size else 0.0:.1f}%" if y_cpu.size else "N/A"
                gpu_status = f"{float(y_gpu[-1]) if y_gpu.size else 0.0:.1f}%" if _np.any(y_gpu) else "Not Available"
            self.cpu_gpu_plot.setTitle(f"CPU: {cpu_status}, GPU: {gpu_status}")

            # Update RAM/VRAM consolidated plot using densified arrays
            self.ram_curve.setData(x_cpu, y_ram)

            # Handle VRAM data (may not be available)
            if _np.any(y_vram):
                self.vram_curve.setData(x_cpu, y_vram)
            else:
                self.vram_curve.setData([], [])  # Clear data

            # Update RAM/VRAM plot title with current values
            if metrics is not None:
                ram_status = f"{float(metrics.get('ram_percent', 0.0)):.1f}%"
                if _np.any(y_vram):
                    vram_status = f"{float(metrics.get('vram_percent', 0.0)):.1f}%"
                else:
                    vram_status = "Not Available"
            else:
                ram_status = f"{float(y_ram[-1]) if y_ram.size else 0.0:.1f}%" if y_ram.size else "N/A"
                vram_status = f"{float(y_vram[-1]) if y_vram.size else 0.0:.1f}%" if _np.any(y_vram) else "Not Available"
            self.ram_vram_plot.setTitle(f"RAM: {ram_status}, VRAM: {vram_status}")

        except Exception as e:
            logger.warning(f"Failed to update PyQtGraph plots: {e}")
    
    def update_fallback_display(self, metrics: dict):
        """
        Update fallback text display.
        
        Args:
            metrics: Dictionary of system metrics
        """
        try:
            display_text = f"""
┌─────────────────────────────────────────────────────────────────┐
│ CPU:  {self.create_text_bar(metrics.get('cpu_percent', 0))} {metrics.get('cpu_percent', 0):5.1f}%
│ RAM:  {self.create_text_bar(metrics.get('ram_percent', 0))} {metrics.get('ram_percent', 0):5.1f}% ({metrics.get('ram_used_gb', 0):.1f}/{metrics.get('ram_total_gb', 0):.1f}GB)
│ GPU:  {self.create_text_bar(metrics.get('gpu_percent', 0))} {metrics.get('gpu_percent', 0):5.1f}%
│ VRAM: {self.create_text_bar(metrics.get('vram_percent', 0))} {metrics.get('vram_percent', 0):5.1f}%
└─────────────────────────────────────────────────────────────────┘
"""
            self.fallback_label.setText(display_text)
            
        except Exception as e:
            logger.warning(f"Failed to update fallback display: {e}")
    
    def update_system_info(self, metrics: dict):
        """
        Update system information display.

        Args:
            metrics: Dictionary of system metrics
        """
        try:
            self.cpu_cores_label[1].setText(str(metrics.get('cpu_cores', 'N/A')))
            self.cpu_freq_label[1].setText(f"{metrics.get('cpu_freq_mhz', 0):.0f} MHz")

            # Update RAM info
            self.ram_total_label[1].setText(f"{metrics.get('ram_total_gb', 0):.1f} GB")
            self.ram_used_label[1].setText(f"{metrics.get('ram_used_gb', 0):.1f} GB")

            # Update GPU info if available
            if 'gpu_name' in metrics:
                gpu_name = metrics.get('gpu_name', 'N/A')
                if len(gpu_name) > 35:
                    gpu_name = gpu_name[:32] + '...'

                self.gpu_name_label[1].setText(gpu_name)
                self.gpu_temp_label[1].setText(f"{metrics.get('gpu_temp', 'N/A')}°C")
                self.vram_label[1].setText(
                    f"{metrics.get('vram_used_mb', 0):.0f} / {metrics.get('vram_total_mb', 0):.0f} MB"
                )

                # Show GPU labels
                self.gpu_name_label[0].show()
                self.gpu_name_label[1].show()
                self.gpu_temp_label[0].show()
                self.gpu_temp_label[1].show()
                self.vram_label[0].show()
                self.vram_label[1].show()
            else:
                # Hide GPU labels if no GPU
                self.gpu_name_label[0].hide()
                self.gpu_name_label[1].hide()
                self.gpu_temp_label[0].hide()
                self.gpu_temp_label[1].hide()
                self.vram_label[0].hide()
                self.vram_label[1].hide()

        except Exception as e:
            logger.warning(f"Failed to update system info: {e}")
    
    def create_text_bar(self, percent: float) -> str:
        """
        Create a text-based progress bar.
        
        Args:
            percent: Percentage value (0-100)
            
        Returns:
            Text progress bar
        """
        bar_length = 20
        filled = int(bar_length * percent / 100)
        bar = '█' * filled + '░' * (bar_length - filled)
        return f"[{bar}]"
    
    def get_ascii_header(self) -> str:
        """
        Get ASCII art header.
        
        Returns:
            ASCII art header string
        """
        return """
 ██████╗ ██████╗ ███████╗███╗   ██╗██╗  ██╗ ██████╗███████╗
██╔═══██╗██╔══██╗██╔════╝████╗  ██║██║  ██║██╔════╝██╔════╝
██║   ██║██████╔╝█████╗  ██╔██╗ ██║███████║██║     ███████╗
██║   ██║██╔═══╝ ██╔══╝  ██║╚██╗██║██╔══██║██║     ╚════██║
╚██████╔╝██║     ███████╗██║ ╚████║██║  ██║╚██████╗███████║
 ╚═════╝ ╚═╝     ╚══════╝╚═╝  ╚═══╝╚═╝  ╚═╝ ╚═════╝╚══════╝
        """
    
    def set_update_interval(self, interval_ms: int):
        """
        Set the update interval for monitoring.

        Args:
            interval_ms: Update interval in milliseconds
        """
        interval_seconds = interval_ms / 1000.0
        self.persistent_monitor.set_update_interval(interval_seconds)

    def update_config(self, new_config):
        """
        Update the widget configuration and apply changes.

        Args:
            new_config: New configuration to apply
        """
        old_config = self.config
        old_monitor_config = self._build_monitor_config(old_config)
        self.config = new_config
        self.monitor_config = self._build_monitor_config(new_config)

        # Check if we need to restart monitoring with new parameters
        if (old_monitor_config.update_fps != self.monitor_config.update_fps or
            old_monitor_config.history_duration_seconds != self.monitor_config.history_duration_seconds):

            logger.info(
                "Updating performance monitor: %.2f FPS, %.2fs history",
                self.monitor_config.update_fps,
                self.monitor_config.history_duration_seconds,
            )

            # Stop current monitoring
            self.stop_monitoring()

            # Recalculate parameters
            update_interval = self.monitor_config.update_interval_seconds
            history_length = self.monitor_config.calculated_max_data_points

            # Create new monitors with updated config
            self.monitor = SystemMonitor(history_length=history_length)
            self.persistent_monitor = PersistentSystemMonitor(
                update_interval=update_interval,
                history_length=history_length
            )

            # Reconnect signals
            self.persistent_monitor.connect_signals(
                metrics_callback=self.on_metrics_updated,
                error_callback=self.on_metrics_error
            )

            # Restart monitoring
            self.start_monitoring()

        # Update render timer FPS if needed
        if getattr(old_monitor_config, "render_fps", None) != getattr(self.monitor_config, "render_fps", None):
            if self._render_timer.isActive():
                self._stop_render_timer()
                self._start_render_timer()

        # Update plot appearance if needed
        if (old_config.performance_monitor.chart_colors != new_config.performance_monitor.chart_colors or
            old_config.performance_monitor.line_width != new_config.performance_monitor.line_width):
            self._update_plot_appearance()

        logger.debug("Performance monitor configuration updated")

    def _update_plot_appearance(self):
        """Update plot appearance based on current configuration."""
        colors = self.monitor_config.chart_colors
        line_width = self.monitor_config.line_width

        # Update curve pens
        self.cpu_curve.setPen(pg.mkPen(colors['cpu'], width=line_width))
        self.ram_curve.setPen(pg.mkPen(colors['ram'], width=line_width))
        self.gpu_curve.setPen(pg.mkPen(colors['gpu'], width=line_width))
        self.vram_curve.setPen(pg.mkPen(colors['vram'], width=line_width))

        # Update plot grid for consolidated plots (don't change X range here - let update_pyqtgraph_plots handle it)
        plots = [self.cpu_gpu_plot, self.ram_vram_plot]
        for plot in plots:
            plot.showGrid(x=self.monitor_config.show_grid, y=self.monitor_config.show_grid, alpha=0.3)
    
    def closeEvent(self, event):
        """Handle widget close event."""
        self.stop_monitoring()
        event.accept()
