#!/usr/bin/env python3
"""AITop - A high performance AI/ML workload monitoring system."""
# pylint: disable=too-many-lines

import argparse
import curses
import json
import logging
import os
import sys
import threading
import time
from dataclasses import asdict, is_dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Optional

from aitop.application.configuration import load_themes
from aitop.core.process.killer import ProcessTerminator

# Import optimized data collector (from earlier artifact)
from aitop.application.process_query import filter_processes
from aitop.data_collector import DataCollector, SystemData
from aitop.infrastructure.file_config_source import FileConfigSource
from aitop.ui.components.cpu_panel import CPUPanel
from aitop.ui.components.footer import FooterComponent
from aitop.ui.components.gpu_panel import GPUPanel
from aitop.ui.components.header import HeaderComponent
from aitop.ui.components.memory_panel import MemoryPanel
from aitop.ui.components.modal import ModalDialog
from aitop.ui.components.overview import OverviewPanel
from aitop.ui.components.process_panel import ProcessPanel
from aitop.ui.components.tabs import TabsComponent
from aitop.ui.display import Display
from aitop.version import __version__

_CONFIG_SOURCE = FileConfigSource()


def setup_logging(debug_mode: bool = False, log_file: str = "aitop.log") -> None:
    """Configure logging with performance optimizations.

    Args:
        debug_mode: If True, enable debug logging to file
        log_file: Path to log file for debug mode
    """
    # Set up root logger
    root_logger = logging.getLogger()

    # Always log warnings and above to console
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.WARNING)
    console_formatter = logging.Formatter("%(levelname)s: %(message)s")
    console_handler.setFormatter(console_formatter)
    root_logger.addHandler(console_handler)

    if debug_mode:
        # Optimize file logging for performance
        file_handler = logging.FileHandler(log_file)
        file_handler.setLevel(logging.DEBUG)

        # More efficient formatter
        file_formatter = logging.Formatter(
            (
                "%(asctime)s.%(msecs)03d [%(threadName)s] "
                "%(name)s:%(lineno)d - %(levelname)s - %(message)s"
            ),
            datefmt="%H:%M:%S",  # Use shorter timestamp for better performance
        )
        file_handler.setFormatter(file_formatter)
        root_logger.addHandler(file_handler)
        root_logger.setLevel(logging.DEBUG)

        # Log basic system information
        import platform

        logging.debug("=== AITop v%s ===", __version__)
        logging.debug(
            "Platform: %s, Python: %s", platform.platform(), sys.version.split()[0]
        )
    else:
        root_logger.setLevel(logging.WARNING)


class AITop:
    """Main application class with performance optimizations."""

    def __init__(self, stdscr: Any) -> None:
        """Initialize the application.

        Args:
            stdscr: curses screen object
        """
        self.stdscr = stdscr
        self.logger = logging.getLogger(self.__class__.__name__)
        self.logger.debug("Initializing AITop application")

        # Application state with thread safety
        self.running = True
        self.selected_tab = 0
        self.sort_by = "cpu_percent"
        self.sort_reverse = True
        self.scroll_position = 0
        self.selected_process_index = (
            0  # For process selection and killing (AI Processes tab)
        )
        self.selected_gpu_process_index = 0  # For GPU tab process selection
        self.selected_overview_process_index = 0  # For Overview tab process selection
        self.process_filter_query = ""
        self.filter_input_mode = False
        self.process_tree_mode = False
        self.needs_redraw = True
        self.render_lock = threading.RLock()

        # Performance settings
        self.input_poll_interval = 0.05  # 50ms input polling (20Hz)
        self.render_interval = 0.2  # 200ms render interval (5 FPS)
        self.last_input_poll = 0.0
        self.last_render = 0.0
        self.last_resize_check = 0.0
        self.resize_check_interval = 0.5  # Check for resize every 500ms

        # Initialize display with better performance
        self.display = Display(stdscr)
        self.last_size = self.display.get_dimensions()

        # Initialize UI components
        self.header = HeaderComponent(self.display)
        self.footer = FooterComponent(self.display)
        self.tabs = TabsComponent(self.display)
        self.overview = OverviewPanel(self.display)
        self.gpu_panel = GPUPanel(self.display)
        self.process_panel = ProcessPanel(self.display)
        self.memory_panel = MemoryPanel(self.display)
        self.cpu_panel = CPUPanel(self.display)
        self.modal = ModalDialog(self.display)

        # Process termination system
        self.terminator = ProcessTerminator(
            ai_process_monitor=None
        )  # Will be set later if AI monitor available

        # Performance metrics for UI
        self.perf_metrics = {
            "render_time": 0.0,
            "collection_time": 0.0,
            "frames_per_second": 0.0,
            "last_fps_calc": time.time(),
            "frame_count": 0,
        }
        self.history_size = 60
        self.metric_history: dict[str, list[float]] = {
            "cpu": [],
            "memory": [],
            "gpu": [],
        }
        self._last_rendered_tab_name: Optional[str] = None

        # Data collector will be initialized and started in _main() with CLI args
        # This prevents double initialization and resource leaks
        self.collector: Optional[DataCollector] = None
        self.system_data: Optional[SystemData] = None

    def _npu_available(self) -> bool:
        """Return True when any NPU vendor is currently detected."""
        collector = getattr(self, "collector", None)
        if collector and any(vendor.endswith("_npu") for vendor in collector.vendors):
            return True

        system_data = getattr(self, "system_data", None)
        if system_data and any(
            vendor.endswith("_npu") for _, vendor in system_data.gpu_info
        ):
            return True

        return False

    def _get_active_tabs(self) -> list[str]:
        """Build the active tab list based on detected accelerators."""
        tabs = ["OVERVIEW", "AI PROCESSES", "GPU"]
        if self._npu_available():
            tabs.append("NPU")
        tabs.extend(["MEMORY", "CPU"])
        return tabs

    def _get_tab_indices(self) -> dict[str, int]:
        """Get a name->index map for currently active tabs."""
        return {name: idx for idx, name in enumerate(self._get_active_tabs())}

    def _ensure_valid_selected_tab(self) -> None:
        """Clamp selected tab index if active tab count changed."""
        active_tabs = self._get_active_tabs()
        if active_tabs and self.selected_tab >= len(active_tabs):
            self.selected_tab = len(active_tabs) - 1
        elif not active_tabs:
            self.selected_tab = 0

    def _split_accelerator_info(
        self,
    ) -> tuple[list[tuple[Any, str]], list[tuple[Any, str]]]:
        """Split accelerator list into GPU and NPU entries."""
        if not self.system_data:
            return [], []

        gpu_info: list[tuple[Any, str]] = []
        npu_info: list[tuple[Any, str]] = []
        for accelerator, vendor in self.system_data.gpu_info:
            if vendor.endswith("_npu"):
                npu_info.append((accelerator, vendor))
            else:
                gpu_info.append((accelerator, vendor))

        return gpu_info, npu_info

    def _is_filterable_tab(self) -> bool:
        """Return True when the current tab supports process filtering."""
        tab_indices = self._get_tab_indices()
        filterable_indices = {
            tab_indices.get("AI PROCESSES"),
            tab_indices.get("GPU"),
            tab_indices.get("NPU"),
        }
        return self.selected_tab in {
            idx for idx in filterable_indices if idx is not None
        }

    def _reset_filter_navigation(self) -> None:
        """Reset selection state affected by filtering changes."""
        self.scroll_position = 0
        self.selected_process_index = 0
        self.selected_gpu_process_index = 0

    def _get_filtered_ai_processes(self) -> list[dict[str, Any]]:
        """Get AI process list filtered by current query."""
        if not self.system_data:
            return []
        return filter_processes(self.system_data.processes, self.process_filter_query)

    def _get_filtered_accelerator_processes(
        self, accelerator_info: list[tuple[Any, str]]
    ) -> list[dict[str, Any]]:
        """Get flattened accelerator process list filtered by query."""
        if not self.system_data:
            return []
        return self.gpu_panel.get_all_gpu_processes(
            accelerator_info, self.process_filter_query
        )

    def _get_filtered_gpu_processes(self) -> list[dict[str, Any]]:
        """Get flattened GPU process list filtered by current query."""
        gpu_info, _npu_info = self._split_accelerator_info()
        return self._get_filtered_accelerator_processes(gpu_info)

    def _get_filtered_npu_processes(self) -> list[dict[str, Any]]:
        """Get flattened NPU process list filtered by current query."""
        _gpu_info, npu_info = self._split_accelerator_info()
        return self._get_filtered_accelerator_processes(npu_info)

    def _handle_filter_input(self, key: int) -> bool:
        """Handle process filter input keys.

        Returns:
            bool: True if key was consumed by filter input handling.
        """
        if not self._is_filterable_tab() and not self.filter_input_mode:
            return False

        if key in (ord("/"), 6) and self._is_filterable_tab():  # 6 = Ctrl+F
            self.filter_input_mode = True
            return True

        if key == 12 and self._is_filterable_tab():  # 12 = Ctrl+L
            if self.filter_input_mode or self.process_filter_query:
                self.filter_input_mode = False
                self.process_filter_query = ""
                self._reset_filter_navigation()
                return True
            return False

        if key == 27 and self._is_filterable_tab():
            if self.filter_input_mode or self.process_filter_query:
                self.filter_input_mode = False
                self.process_filter_query = ""
                self._reset_filter_navigation()
                return True
            return False

        if not self.filter_input_mode:
            return False

        if key in (curses.KEY_ENTER, 10, 13):
            self.filter_input_mode = False
            return True

        if key in (curses.KEY_BACKSPACE, 127, 8):
            if self.process_filter_query:
                self.process_filter_query = self.process_filter_query[:-1]
                self._reset_filter_navigation()
            return True

        if 32 <= key <= 126:
            self.process_filter_query += chr(key)
            self._reset_filter_navigation()
            return True

        # While filter input mode is active, consume unhandled keys.
        return True

    def _get_filter_match_counts(self) -> tuple[int, int]:
        """Get (matches, total) process counts for current filterable tab."""
        if not self.system_data:
            return (0, 0)

        tab_indices = self._get_tab_indices()
        ai_tab = tab_indices.get("AI PROCESSES")
        gpu_tab = tab_indices.get("GPU")
        npu_tab = tab_indices.get("NPU")

        if ai_tab is not None and self.selected_tab == ai_tab:
            total = len(self.system_data.processes)
            matches = len(self._get_filtered_ai_processes())
            return (matches, total)

        if gpu_tab is not None and self.selected_tab == gpu_tab:
            gpu_info, _npu_info = self._split_accelerator_info()
            total = len(self.gpu_panel.get_all_gpu_processes(gpu_info))
            matches = len(self._get_filtered_gpu_processes())
            return (matches, total)

        if npu_tab is not None and self.selected_tab == npu_tab:
            _gpu_info, npu_info = self._split_accelerator_info()
            total = len(self.gpu_panel.get_all_gpu_processes(npu_info))
            matches = len(self._get_filtered_npu_processes())
            return (matches, total)

        return (0, 0)

    def _clamp_filtered_selection(self) -> None:
        """Clamp selection indices to current filtered result sizes."""
        tab_indices = self._get_tab_indices()
        ai_tab = tab_indices.get("AI PROCESSES")
        gpu_tab = tab_indices.get("GPU")
        npu_tab = tab_indices.get("NPU")

        if ai_tab is not None and self.selected_tab == ai_tab:
            filtered_processes = self._get_filtered_ai_processes()
            if not filtered_processes:
                self.selected_process_index = 0
                self.scroll_position = 0
                return
            self.selected_process_index = min(
                self.selected_process_index, len(filtered_processes) - 1
            )
            return

        if (gpu_tab is not None and self.selected_tab == gpu_tab) or (
            npu_tab is not None and self.selected_tab == npu_tab
        ):
            if gpu_tab is not None and self.selected_tab == gpu_tab:
                filtered_processes = self._get_filtered_gpu_processes()
            else:
                filtered_processes = self._get_filtered_npu_processes()
            if not filtered_processes:
                self.selected_gpu_process_index = 0
                return
            self.selected_gpu_process_index = min(
                self.selected_gpu_process_index, len(filtered_processes) - 1
            )

    def _render_filter_status(self, start_y: int = 3, indent: int = 2) -> int:
        """Render filter status line and return next content row."""
        if not self._is_filterable_tab():
            return start_y

        match_count, total_count = self._get_filter_match_counts()
        filter_value = (
            self.process_filter_query if self.process_filter_query else "<all>"
        )
        if self.filter_input_mode:
            filter_line = (
                f"[SEARCH] {filter_value}  [{match_count}/{total_count}]  "
                "Enter apply | Esc clear | Backspace edit"
            )
            attr = self.display.get_theme_attr("chip_active")
        else:
            filter_line = (
                f"[SEARCH] {filter_value}  [{match_count}/{total_count}]  "
                "/ or Ctrl+F edit | fields: name:,pid:,user:,gpu:,cmd:"
            )
            attr = self.display.get_theme_attr("muted")

        self.display.safe_addstr(start_y, indent, filter_line, attr)
        return start_y + 1

    def _record_history_sample(self) -> None:
        """Store a rolling history sample for mini trend charts."""
        if not self.system_data:
            return

        cpu_value = (
            float(self.system_data.cpu_stats.total_percent)
            if self.system_data.cpu_stats
            else 0.0
        )
        memory_value = (
            float(self.system_data.memory_stats.percent)
            if self.system_data.memory_stats
            else 0.0
        )
        gpu_values = [float(gpu.utilization) for gpu, _ in self.system_data.gpu_info]
        gpu_value = sum(gpu_values) / len(gpu_values) if gpu_values else 0.0

        for key, value in (
            ("cpu", cpu_value),
            ("memory", memory_value),
            ("gpu", gpu_value),
        ):
            self.metric_history[key].append(value)
            if len(self.metric_history[key]) > self.history_size:
                self.metric_history[key] = self.metric_history[key][
                    -self.history_size :
                ]

    def _build_help_lines(self) -> list[str]:
        """Build dynamic help text lines for the modal overlay."""
        lines = [
            "Navigation: Left/Right tabs, Up/Down select process.",
            "Sorting: c=CPU, m=MEM, h=toggle sort order.",
            "Actions: k=kill selected process, r=force redraw.",
            "Settings: o opens theme settings.",
            "Search: / or Ctrl+F edit filter, Esc/Ctrl+L clear.",
            "Filter examples:",
            "  name:python user:alice",
            "  pid:1234 cmd:train.py",
            "  gpu:true status:running",
            "Snapshot: Shift+S writes JSON in current directory.",
            "Quit: q",
        ]
        terminator = getattr(self, "terminator", None)
        if terminator:
            if terminator.can_attempt_sudo():
                lines.insert(
                    5,
                    "Privileged retries: enabled (sudo password prompt).",
                )
            else:
                lines.insert(
                    5,
                    "Privileged retries: unavailable (sudo/kill executable missing).",
                )
        ai_tab = self._get_tab_indices().get("AI PROCESSES")
        if ai_tab is not None and self.selected_tab == ai_tab:
            mode = "ON" if self.process_tree_mode else "OFF"
            lines.insert(4, f"AI tab: t toggles process tree mode (currently {mode}).")
        return lines

    def _show_help_overlay(self) -> None:
        """Render keyboard help overlay."""
        self.modal.render_text_dialog("AITop Help", self._build_help_lines())

    def _open_settings_menu(self) -> None:
        """Open settings menu and apply selected theme."""
        available_themes = sorted(getattr(self.display, "themes", {}).keys())
        if not available_themes:
            self.modal.render_error("No themes available")
            return

        selected_theme = self.modal.render_theme_menu(
            available_themes,
            getattr(self.display, "current_theme", "default"),
        )
        if not selected_theme:
            return

        if self.display.reload_theme(selected_theme):
            self.display.force_redraw()
            self.needs_redraw = True
            self.modal.render_status(
                f"Theme set to {selected_theme}", success=True, duration_ms=900
            )
            return

        self.modal.render_error(f"Failed to load theme: {selected_theme}")

    @staticmethod
    def _to_json_ready(value: Any) -> Any:
        """Convert dataclass values to plain structures for JSON output."""
        if is_dataclass(value) and not isinstance(value, type):
            return asdict(value)
        return value

    def _build_snapshot_payload(self) -> dict[str, Any]:
        """Build snapshot payload for export."""
        if not self.system_data:
            raise RuntimeError("No system data available yet")

        gpu_entries = []
        for gpu, vendor in self.system_data.gpu_info:
            gpu_payload = self._to_json_ready(gpu)
            if isinstance(gpu_payload, dict):
                gpu_payload["vendor"] = vendor
                gpu_entries.append(gpu_payload)
            else:
                gpu_entries.append({"vendor": vendor, "value": str(gpu_payload)})

        return {
            "timestamp": datetime.now().isoformat(timespec="seconds"),
            "version": __version__,
            "ui_state": {
                "selected_tab": self.selected_tab,
                "sort_by": self.sort_by,
                "sort_reverse": self.sort_reverse,
                "filter_query": self.process_filter_query,
                "tree_mode": self.process_tree_mode,
            },
            "system": {
                "primary_vendor": self.system_data.primary_vendor,
                "cpu": self._to_json_ready(self.system_data.cpu_stats),
                "memory": self._to_json_ready(self.system_data.memory_stats),
                "memory_types": self._to_json_ready(self.system_data.memory_types),
                "gpus": gpu_entries,
                "processes": self.system_data.processes,
            },
            "history": self.metric_history,
        }

    def _export_snapshot(self) -> None:
        """Export current data snapshot to JSON."""
        try:
            payload = self._build_snapshot_payload()
            filename = f"aitop-snapshot-{datetime.now().strftime('%Y%m%d-%H%M%S')}.json"
            output_path = Path.cwd() / filename
            with output_path.open("w", encoding="utf-8") as output_file:
                json.dump(payload, output_file, indent=2, sort_keys=True, default=str)
            self.modal.render_status(
                f"Snapshot saved: {filename}", success=True, duration_ms=1200
            )
            self.logger.info("Snapshot exported to %s", output_path)
        except Exception as error:
            self.logger.error("Snapshot export failed: %s", error, exc_info=True)
            self.modal.render_error(f"Snapshot failed: {error}", duration_ms=1600)

    def _maybe_retry_kill_with_sudo(
        self, pid: int, signal_num: int, result: dict[str, Any]
    ) -> dict[str, Any]:
        """Offer an explicit privileged retry for root-required process actions."""
        if result["success"] or not result.get("requires_root"):
            return result
        if not result.get("elevation_available"):
            result["errors"].append("Privileged retry unavailable: sudo/kill not found")
            result["message"] = "Root privileges required and sudo is unavailable"
            return result

        signal_name = str(signal_num)
        for signal_info in ProcessTerminator.get_available_signals():
            if int(signal_info["num"]) == int(signal_num):
                signal_name = str(signal_info["name"])
                break

        confirm = self.modal.render_confirmation(
            "Permission denied for normal kill.\n"
            f"Retry with sudo (signal {signal_name}, PID {pid})?\n\n"
            "You will be asked for sudo password in-app.",
            title="Privileged Kill",
        )
        if not confirm:
            return result

        sudo_password = self.modal.render_password_prompt(
            "Enter sudo password for privileged kill.\n"
            "Input is hidden and used only for this action.",
            title="Sudo Authentication",
        )
        if sudo_password is None:
            return result

        elevated_result = self.terminator.terminate_process_with_elevation(
            pid=pid,
            signal_num=signal_num,
            force=False,
            sudo_password=sudo_password,
        )
        sudo_password = None
        return elevated_result

    def handle_input(self) -> None:
        """Handle user input efficiently."""
        try:
            # Only poll for input at specified interval
            current_time = time.time()
            if current_time - self.last_input_poll < self.input_poll_interval:
                return

            self.last_input_poll = current_time
            key = self.display.stdscr.getch()

            if key == curses.ERR:  # No input available
                return

            self.needs_redraw = True  # Input requires redraw
            self._ensure_valid_selected_tab()
            active_tabs = self._get_active_tabs()
            tab_indices = {name: idx for idx, name in enumerate(active_tabs)}

            if self._handle_filter_input(key):
                return

            if key == ord("q"):
                self.running = False
                self.logger.debug("User requested exit")
            elif key == ord("c"):
                self.sort_by = "cpu_percent"
                self.sort_reverse = True
            elif key == ord("m"):
                self.sort_by = "memory_percent"
                self.sort_reverse = True
            elif key == ord("h"):
                self.sort_reverse = not self.sort_reverse
            elif key == ord("t") and self.selected_tab == tab_indices.get(
                "AI PROCESSES", -1
            ):
                self.process_tree_mode = not self.process_tree_mode
                self.scroll_position = 0
                self.selected_process_index = 0
                mode = "enabled" if self.process_tree_mode else "disabled"
                self.modal.render_status(
                    f"Process tree mode {mode}", success=True, duration_ms=800
                )
            elif key == ord("?"):
                self._show_help_overlay()
            elif key == ord("o"):
                self._open_settings_menu()
            elif key == ord("S"):
                self._export_snapshot()
            elif key == ord("r"):  # Add refresh key
                self.display.force_redraw()
                self.logger.debug("Manual refresh requested")
            elif key == curses.KEY_RESIZE:  # Handle terminal resize
                self.display.handle_resize()
                self.needs_redraw = True
                self.last_size = self.display.get_dimensions()
                self.logger.debug("Terminal resize event handled")
            elif key == ord("k"):  # Kill process
                overview_tab = tab_indices.get("OVERVIEW", -1)
                ai_tab = tab_indices.get("AI PROCESSES", -1)
                gpu_tab = tab_indices.get("GPU", -1)
                npu_tab = tab_indices.get("NPU", -1)

                if self.selected_tab == overview_tab and self.system_data:
                    self.handle_overview_kill_request()
                elif self.selected_tab == ai_tab and self.system_data:
                    self.handle_kill_request()
                elif self.selected_tab == gpu_tab and self.system_data:
                    gpu_info, _npu_info = self._split_accelerator_info()
                    self.handle_gpu_kill_request(gpu_info, device_label="GPU")
                elif self.selected_tab == npu_tab and self.system_data:
                    _gpu_info, npu_info = self._split_accelerator_info()
                    self.handle_gpu_kill_request(npu_info, device_label="NPU")
            elif key in [curses.KEY_LEFT, curses.KEY_RIGHT]:
                self.selected_tab = self.tabs.handle_tab_input(
                    key, self.selected_tab, custom_tabs=active_tabs
                )
                self.scroll_position = 0
                self.selected_process_index = 0  # Reset selection when changing tabs
                self.selected_gpu_process_index = 0
                self.selected_overview_process_index = 0
                self.filter_input_mode = False
            elif key in [curses.KEY_UP, curses.KEY_DOWN] and self.system_data:
                overview_tab = tab_indices.get("OVERVIEW", -1)
                ai_tab = tab_indices.get("AI PROCESSES", -1)
                gpu_tab = tab_indices.get("GPU", -1)
                npu_tab = tab_indices.get("NPU", -1)

                if self.selected_tab == overview_tab:  # Overview tab
                    # Navigate through top processes (max 5 shown)
                    # Sort processes by CPU to match what's displayed
                    sorted_processes = sorted(
                        self.system_data.processes,
                        key=lambda p: p["cpu_percent"],
                        reverse=True,
                    )
                    max_processes = min(5, len(sorted_processes))
                    if max_processes > 0:
                        if key == curses.KEY_UP:
                            self.selected_overview_process_index = max(
                                0, self.selected_overview_process_index - 1
                            )
                        elif key == curses.KEY_DOWN:
                            self.selected_overview_process_index = min(
                                max_processes - 1,
                                self.selected_overview_process_index + 1,
                            )
                elif self.selected_tab == ai_tab:  # AI Processes tab
                    # Update both scroll position and selection index together
                    filtered_processes = self._get_filtered_ai_processes()
                    if filtered_processes:
                        old_pos = self.selected_process_index
                        if key == curses.KEY_UP:
                            self.selected_process_index = max(
                                0, self.selected_process_index - 1
                            )
                        elif key == curses.KEY_DOWN:
                            self.selected_process_index = min(
                                len(filtered_processes) - 1,
                                self.selected_process_index + 1,
                            )
                        # Scroll position follows selection
                        if self.selected_process_index != old_pos:
                            self.scroll_position = self.process_panel.handle_scroll(
                                key, self.scroll_position, filtered_processes
                            )
                    else:
                        self.selected_process_index = 0
                        self.scroll_position = 0
                elif self.selected_tab in {gpu_tab, npu_tab}:  # Accelerator tabs
                    if self.selected_tab == gpu_tab:
                        accelerator_info, _npu_info = self._split_accelerator_info()
                    else:
                        _gpu_info, accelerator_info = self._split_accelerator_info()
                    accelerator_processes = self._get_filtered_accelerator_processes(
                        accelerator_info
                    )
                    if accelerator_processes:
                        if key == curses.KEY_UP:
                            self.selected_gpu_process_index = max(
                                0, self.selected_gpu_process_index - 1
                            )
                        elif key == curses.KEY_DOWN:
                            self.selected_gpu_process_index = min(
                                len(accelerator_processes) - 1,
                                self.selected_gpu_process_index + 1,
                            )
        except Exception as e:
            self.logger.error(f"Input handling error: {e}", exc_info=True)

    def handle_kill_request(self) -> None:
        """Handle user request to kill a process from AI Processes tab."""
        if not self.system_data:
            return

        try:
            filtered_processes = self._get_filtered_ai_processes()
            if not filtered_processes:
                self.modal.render_error("No matching process selected")
                return

            # Get the selected process
            selected_process = self.process_panel.get_selected_process(
                filtered_processes,
                self.selected_process_index,
                self.sort_by,
                self.sort_reverse,
                self.process_tree_mode,
            )

            if not selected_process:
                self.modal.render_error("No process selected")
                return

            # Build process info for modal
            process_info = {
                "pid": selected_process["pid"],
                "name": selected_process["name"],
                "username": selected_process.get("username", "unknown"),
            }

            # Show signal selection menu
            signals = ProcessTerminator.get_available_signals()
            selected_signal = self.modal.render_signal_menu(signals, process_info)

            if selected_signal is None:
                # User cancelled
                return

            # Attempt to terminate the process
            result = self.terminator.terminate_process(
                pid=selected_process["pid"], signal_num=selected_signal, force=False
            )
            result = self._maybe_retry_kill_with_sudo(
                selected_process["pid"], selected_signal, result
            )

            # Show warnings if any (before attempting kill)
            if result["warnings"]:
                if not self.modal.render_warning_list(
                    result["warnings"], "AI Process Warning"
                ):
                    # User chose not to continue
                    return

            # Show result
            if result["success"]:
                self.modal.render_status(result["message"], success=True)
                self.logger.info(f"Process kill: {result['message']}")
            else:
                error_msg = result["message"]
                if result["errors"]:
                    error_msg = result["errors"][0]
                self.modal.render_error(error_msg)
                self.logger.warning(f"Process kill failed: {error_msg}")

        except Exception as e:
            self.logger.error(f"Kill request error: {e}", exc_info=True)
            self.modal.render_error(f"Error: {str(e)}")

    def handle_overview_kill_request(self) -> None:
        """Handle user request to kill a process from Overview tab."""
        if not self.system_data or not self.system_data.processes:
            return

        try:
            # Get the selected process from overview
            selected_process = self.overview.get_selected_process(
                self.system_data.processes, self.selected_overview_process_index
            )

            if not selected_process:
                self.modal.render_error("No process selected")
                return

            # Build process info for modal
            process_info = {
                "pid": selected_process["pid"],
                "name": selected_process["name"],
                "username": selected_process.get("username", "unknown"),
            }

            # Show signal selection menu
            signals = ProcessTerminator.get_available_signals()
            selected_signal = self.modal.render_signal_menu(signals, process_info)

            if selected_signal is None:
                # User cancelled
                return

            # Attempt to terminate the process
            result = self.terminator.terminate_process(
                pid=selected_process["pid"], signal_num=selected_signal, force=False
            )
            result = self._maybe_retry_kill_with_sudo(
                selected_process["pid"], selected_signal, result
            )

            # Show warnings if any (before attempting kill)
            if result["warnings"]:
                if not self.modal.render_warning_list(
                    result["warnings"], "AI Process Warning"
                ):
                    # User chose not to continue
                    return

            # Show result
            if result["success"]:
                self.modal.render_status(result["message"], success=True)
                self.logger.info(f"Overview process kill: {result['message']}")
            else:
                error_msg = result["message"]
                if result["errors"]:
                    error_msg = result["errors"][0]
                self.modal.render_error(error_msg)
                self.logger.warning(f"Overview process kill failed: {error_msg}")

        except Exception as e:
            self.logger.error(f"Overview kill request error: {e}", exc_info=True)
            self.modal.render_error(f"Error: {str(e)}")

    def handle_gpu_kill_request(
        self,
        accelerator_info: Optional[list[tuple[Any, str]]] = None,
        device_label: str = "GPU",
    ) -> None:
        """Handle user request to kill a process from accelerator tabs."""
        if not self.system_data:
            return

        active_info = accelerator_info or self.system_data.gpu_info
        if not active_info:
            return

        try:
            # Get the selected accelerator process
            selected_process = self.gpu_panel.get_selected_process_by_index(
                active_info,
                self.selected_gpu_process_index,
                self.process_filter_query,
            )

            if not selected_process:
                self.modal.render_error(f"No {device_label} process selected")
                return

            # Build process info for modal (accelerator process payload is limited)
            process_info = {
                "pid": selected_process["pid"],
                "name": selected_process["name"],
                "username": "unknown",  # Accelerator info doesn't include username
            }

            # Show signal selection menu
            signals = ProcessTerminator.get_available_signals()
            selected_signal = self.modal.render_signal_menu(signals, process_info)

            if selected_signal is None:
                # User cancelled
                return

            # Attempt to terminate the process
            result = self.terminator.terminate_process(
                pid=selected_process["pid"], signal_num=selected_signal, force=False
            )
            result = self._maybe_retry_kill_with_sudo(
                selected_process["pid"], selected_signal, result
            )

            # Show warnings if any
            if result["warnings"]:
                if not self.modal.render_warning_list(
                    result["warnings"], f"{device_label} Process Warning"
                ):
                    # User chose not to continue
                    return

            # Show result
            if result["success"]:
                self.modal.render_status(result["message"], success=True)
                self.logger.info("%s process kill: %s", device_label, result["message"])
            else:
                error_msg = result["message"]
                if result["errors"]:
                    error_msg = result["errors"][0]
                self.modal.render_error(error_msg)
                self.logger.warning(
                    "%s process kill failed: %s", device_label, error_msg
                )

        except Exception as e:
            self.logger.error(
                "%s kill request error: %s", device_label, e, exc_info=True
            )
            self.modal.render_error(f"Error: {str(e)}")

    def check_resize(self) -> bool:
        """Check if terminal has been resized.

        Returns:
            bool: True if resized, False otherwise
        """
        current_time = time.time()
        if current_time - self.last_resize_check < self.resize_check_interval:
            return False

        self.last_resize_check = current_time
        current_size = self.display.get_dimensions()

        if current_size != self.last_size:
            self.display.handle_resize()
            self.needs_redraw = True
            self.last_size = current_size
            self.logger.debug(
                f"Terminal resized to {current_size[1]}x{current_size[0]}"
            )
            return True

        return False

    def update_data(self) -> bool:
        """Update system data from collector.

        Returns:
            bool: True if data was updated, False otherwise
        """
        # Check if collector is initialized
        if not self.collector:
            return False

        # Get latest data if available
        new_data = self.collector.get_data(timeout=0.01)
        if new_data:
            self.system_data = new_data
            self._record_history_sample()
            self.needs_redraw = True
            return True
        return False

    def _get_footer_controls(self) -> list[tuple[str, str]]:
        """Get footer controls based on current tab.

        Returns:
            List of (key, action) tuples for footer
        """
        base_controls = [
            ("q", "quit"),
            ("?", "help"),
            ("o", "settings"),
            ("S", "snapshot"),
            ("c", "sort CPU"),
            ("m", "sort MEM"),
            ("h", "toggle sort"),
            ("arrows", "navigate"),
        ]

        tab_indices = self._get_tab_indices()
        overview_tab = tab_indices.get("OVERVIEW")
        ai_tab = tab_indices.get("AI PROCESSES")
        gpu_tab = tab_indices.get("GPU")
        npu_tab = tab_indices.get("NPU")

        # Add kill control for Overview, AI Processes, and accelerator tabs
        if self.selected_tab in {
            idx for idx in (overview_tab, ai_tab, gpu_tab, npu_tab) if idx is not None
        }:
            base_controls.insert(1, ("k", "kill"))

        # Tree mode toggle for AI process list
        if ai_tab is not None and self.selected_tab == ai_tab:
            base_controls.insert(4, ("t", "tree"))

        # Process filter controls for AI Processes and accelerator tabs
        if self.selected_tab in {
            idx for idx in (ai_tab, gpu_tab, npu_tab) if idx is not None
        }:
            base_controls.insert(2, ("ctrl+f", "search"))
            base_controls.insert(3, ("esc/ctrl+l", "clear"))

        return base_controls

    def update_performance_metrics(self, render_time: float) -> None:
        """Update performance tracking metrics."""
        self.perf_metrics["render_time"] = render_time

        # Update FPS calculation
        current_time = time.time()
        self.perf_metrics["frame_count"] += 1

        # Calculate FPS every second
        elapsed = current_time - self.perf_metrics["last_fps_calc"]
        if elapsed >= 1.0:
            self.perf_metrics["frames_per_second"] = (
                self.perf_metrics["frame_count"] / elapsed
            )
            self.perf_metrics["frame_count"] = 0
            self.perf_metrics["last_fps_calc"] = current_time

    def render(self) -> None:
        """Render the complete interface with optimizations."""
        # Skip rendering if not needed or not time yet
        current_time = time.time()
        if (
            not self.needs_redraw
            and current_time - self.last_render < self.render_interval
        ):
            return

        # Check for data to render
        if not self.system_data:
            return

        render_start = time.time()
        with self.render_lock:  # Thread safety for rendering
            try:
                self._ensure_valid_selected_tab()
                active_tabs = self._get_active_tabs()
                tab_indices = {name: idx for idx, name in enumerate(active_tabs)}
                gpu_info, npu_info = self._split_accelerator_info()
                current_tab_name = (
                    active_tabs[self.selected_tab]
                    if 0 <= self.selected_tab < len(active_tabs)
                    else None
                )
                tab_changed = current_tab_name != getattr(
                    self, "_last_rendered_tab_name", None
                )

                begin_frame = getattr(self.display, "begin_frame", None)
                end_frame = getattr(self.display, "end_frame", None)
                if tab_changed:
                    # Full clear when switching tabs avoids stale artifacts from
                    # radically different layouts while preserving diff rendering
                    # for steady-state updates.
                    self.display.clear()
                if callable(begin_frame):
                    begin_frame()
                elif not tab_changed:
                    # Compatibility fallback for simple display stubs.
                    self.display.clear()

                # Render common elements
                self.header.render(
                    self.system_data.primary_vendor,
                    current_tab=current_tab_name,
                )
                self.tabs.render(self.selected_tab, 1, custom_tabs=active_tabs)

                # Render tab-specific content
                if self.selected_tab == tab_indices.get("OVERVIEW", -1):  # Overview
                    self.overview.render(
                        self.system_data.gpu_info,
                        self.system_data.processes,
                        self.system_data.memory_stats,
                        self.system_data.cpu_stats,
                        self.system_data.primary_vendor,
                        selected_index=self.selected_overview_process_index,
                        history=self.metric_history,
                        dependency_warnings=getattr(
                            self.system_data, "dependency_warnings", []
                        ),
                    )
                elif self.selected_tab == tab_indices.get("AI PROCESSES", -1):
                    self._clamp_filtered_selection()
                    content_start_y = self._render_filter_status(3, 2)
                    filtered_processes = self._get_filtered_ai_processes()
                    if not filtered_processes and self.process_filter_query:
                        self.display.safe_addstr(
                            content_start_y + 1,
                            2,
                            (
                                "No AI processes match filter. "
                                "Press Esc or Ctrl+L to clear."
                            ),
                            curses.color_pair(3),
                        )
                    else:
                        self.process_panel.render(
                            filtered_processes,
                            [gpu for gpu, _ in self.system_data.gpu_info],
                            content_start_y,
                            2,
                            self.sort_by,
                            self.sort_reverse,
                            self.scroll_position,
                            self.selected_process_index,
                            self.process_tree_mode,
                            accelerator_info=self.system_data.gpu_info,
                        )
                elif self.selected_tab == tab_indices.get("GPU", -1):
                    self._clamp_filtered_selection()
                    content_start_y = self._render_filter_status(3, 2)
                    filtered_gpu_processes = self._get_filtered_gpu_processes()
                    # Get selected process PID for highlighting
                    selected_process = self.gpu_panel.get_selected_process_by_index(
                        gpu_info,
                        self.selected_gpu_process_index,
                        self.process_filter_query,
                    )
                    selected_pid = selected_process["pid"] if selected_process else None
                    if not filtered_gpu_processes and self.process_filter_query:
                        self.display.safe_addstr(
                            content_start_y + 1,
                            2,
                            (
                                "No GPU processes match filter. "
                                "Press Esc or Ctrl+L to clear."
                            ),
                            curses.color_pair(3),
                        )
                    else:
                        self.gpu_panel.render(
                            gpu_info,
                            content_start_y,
                            2,
                            selected_pid,
                            self.process_filter_query,
                            device_label="GPU",
                        )
                elif self.selected_tab == tab_indices.get("NPU", -1):
                    self._clamp_filtered_selection()
                    content_start_y = self._render_filter_status(3, 2)
                    filtered_npu_processes = self._get_filtered_npu_processes()
                    selected_process = self.gpu_panel.get_selected_process_by_index(
                        npu_info,
                        self.selected_gpu_process_index,
                        self.process_filter_query,
                    )
                    selected_pid = selected_process["pid"] if selected_process else None
                    if not filtered_npu_processes and self.process_filter_query:
                        self.display.safe_addstr(
                            content_start_y + 1,
                            2,
                            (
                                "No NPU processes match filter. "
                                "Press Esc or Ctrl+L to clear."
                            ),
                            curses.color_pair(3),
                        )
                    else:
                        self.gpu_panel.render(
                            npu_info,
                            content_start_y,
                            2,
                            selected_pid,
                            self.process_filter_query,
                            device_label="NPU",
                        )
                elif self.selected_tab == tab_indices.get("MEMORY", -1):
                    self.memory_panel.render(
                        self.system_data.memory_stats, self.system_data.memory_types
                    )
                elif self.selected_tab == tab_indices.get("CPU", -1):
                    self.cpu_panel.render(self.system_data.cpu_stats)

                # Render footer with tab-specific controls
                footer_controls = self._get_footer_controls()
                self.footer.render(custom_controls=footer_controls)
                if callable(end_frame):
                    end_frame()
                self.display.refresh()

                # Update metrics and flags
                render_time = time.time() - render_start
                self.update_performance_metrics(render_time)
                self.needs_redraw = False
                self.last_render = time.time()
                self._last_rendered_tab_name = current_tab_name

                self.logger.debug(f"UI rendered in {render_time:.3f}s")
            except Exception as e:
                self.logger.error(f"Render error: {e}", exc_info=True)

    def cleanup(self) -> None:
        """Clean up resources."""
        self.logger.debug("Cleaning up application")
        if self.collector:
            self.collector.stop()

        if self.display:
            self.display.cleanup()

    def run(self) -> None:
        """Main application loop with performance optimizations."""
        self.logger.debug("Application run loop started")
        try:
            # Use adaptive timing logic
            while self.running:
                # Check for terminal resize (less frequent)
                self.check_resize()

                # Handle input (moderate frequency)
                self.handle_input()

                # Update data (when available)
                self.update_data()

                # Render UI (controlled by render_interval)
                self.render()

                # Sleep to reduce CPU usage - adaptive sleep
                if self.needs_redraw:
                    # Shorter sleep when UI needs updating
                    time.sleep(0.01)
                else:
                    # Longer sleep when idle
                    time.sleep(0.05)

        except KeyboardInterrupt:
            self.logger.debug("Application interrupted by user")
        except Exception as e:
            self.logger.error(f"Run loop error: {e}", exc_info=True)
        finally:
            self.cleanup()
            self.logger.debug("Application terminated")


def _main(stdscr: Any, args: argparse.Namespace) -> int:
    """Initialize and run the application with custom parameters.

    Args:
        stdscr: Curses screen object
        args: Command line arguments

    Returns:
        int: Exit code (0 for success, non-zero for error)
    """
    try:
        # Update AITop initialization with command line parameters
        app = AITop(stdscr)

        # Apply custom intervals from command line
        app.render_interval = args.render_interval

        # Configure data collector with command line arguments
        collector = DataCollector(
            update_interval=args.update_interval,
            process_interval=args.process_interval,
            gpu_info_interval=args.gpu_interval,
            max_workers=args.workers,
        )
        app.collector = collector

        # Configure adaptive timing if specified
        if args.no_adaptive_timing and hasattr(collector, "_adaptive_timing"):
            collector._adaptive_timing = False

        # Start collector
        collector.start()
        app.logger.debug("DataCollector thread started with custom parameters")

        # Run the application
        app.run()
        return 0
    except Exception as e:
        logging.error(f"Main error: {e}", exc_info=True)
        return 1


def main() -> int:
    """Entry point for the application with enhanced error handling."""
    # Load themes early for CLI help text
    try:
        available_themes = load_themes(source=_CONFIG_SOURCE)
        theme_names = sorted(available_themes.keys())
        theme_list = ", ".join(theme_names)
    except Exception:
        # Fallback if theme loading fails
        theme_list = (
            "default, monokai_pro, nord, solarized_dark, material_ocean, "
            "graphite_modern"
        )
        available_themes = {}

    parser = argparse.ArgumentParser(
        description="AITop - A high performance system monitor for AI/ML workloads",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    # Basic options
    parser.add_argument(
        "--debug", action="store_true", help="Enable debug logging to aitop.log"
    )
    parser.add_argument(
        "--log-file", default="aitop.log", help="Path to log file for debug mode"
    )

    # Performance tuning options
    parser.add_argument(
        "--update-interval",
        type=float,
        default=0.5,
        help="Base data update interval in seconds (default: 0.5)",
    )
    parser.add_argument(
        "--process-interval",
        type=float,
        default=2.0,
        help="Process data collection interval in seconds (default: 2.0)",
    )
    parser.add_argument(
        "--gpu-interval",
        type=float,
        default=1.0,
        help="Full GPU info update interval in seconds (default: 1.0)",
    )
    parser.add_argument(
        "--render-interval",
        type=float,
        default=0.2,
        help="UI render interval in seconds (default: 0.2)",
    )
    parser.add_argument(
        "--workers",
        type=int,
        default=3,
        help="Number of worker threads for data collection (default: 3)",
    )

    # Display options
    parser.add_argument(
        "--theme",
        type=str,
        help=f"Override theme selection. Available: {theme_list}",
    )
    parser.add_argument(
        "--list-themes",
        action="store_true",
        help="List all available themes with descriptions and exit",
    )
    parser.add_argument(
        "--no-adaptive-timing",
        action="store_true",
        help="Disable adaptive timing based on system load",
    )
    args = parser.parse_args()

    # Handle --list-themes before entering curses mode
    if args.list_themes:
        print("\nAvailable AITop Themes:\n")
        if available_themes:
            # Find longest theme name for alignment
            max_name_len = max(len(name) for name in theme_names)
            for theme_name in theme_names:
                theme = available_themes[theme_name]
                desc = theme.get("description", "No description")
                print(f"  {theme_name:<{max_name_len}}  {desc}")
        else:
            print("  Error loading themes. Please check themes.json")
        print("\nUse --theme <name> to select a theme.")
        print("Or set AITOP_THEME environment variable.\n")
        return 0

    # Configure logging based on debug flag
    setup_logging(args.debug, args.log_file)

    # Validate and set theme environment variable if specified
    if args.theme:
        if available_themes and args.theme not in available_themes:
            logging.warning(f"Unknown theme: '{args.theme}'")
            # Find similar theme names
            similar = [t for t in theme_names if args.theme.lower() in t.lower()]
            if similar:
                print(f"Error: Unknown theme '{args.theme}'")
                print(f"Did you mean: {', '.join(similar)}?")
            else:
                print(f"Error: Unknown theme '{args.theme}'")
                print(f"Available themes: {theme_list}")
            print("Run 'aitop --list-themes' for detailed information.")
            return 1
        os.environ["AITOP_THEME"] = args.theme
        logging.debug(f"Setting theme from command line: {args.theme}")

    try:
        # Pass args to curses wrapper
        return curses.wrapper(lambda stdscr: _main(stdscr, args))
    except KeyboardInterrupt:
        logging.debug("Application interrupted by user")
        return 0
    except curses.error as e:
        logging.error(f"Curses error: {e}", exc_info=True)
        print(f"Terminal error: {e}")
        print("Please make sure your terminal supports at least 80x24 characters.")
        return 1
    except Exception as e:
        logging.error(f"Unhandled exception: {e}", exc_info=True)
        print(f"Fatal error: {e}")
        return 1


if __name__ == "__main__":
    sys.exit(main())
